dotnet / dotnet/aspnetcore

[Blazor] Aspire Dashboard Native AOT support

Open
#68,332 0 comments 2 reactions 0 assignees View on GitHub
area-blazor design-proposal NativeAOT
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 5h
Merged PRs (30d)
276

Description

# Detailed design
Aspire Dashboard Native AOT support lets the .NET Aspire Dashboard publish as a native Interactive Server application without depending on reflection or runtime code generation for the Dashboard's validated Blazor feature graph. Today the Dashboard reaches component discovery, activation, parameter assignment, binding, JSON serialization, JavaScript dispatch, persistence, and framework component paths that are reflection-dependent or invisible to the Native AOT compiler. The feature adds compile-time metadata for those paths and makes the framework consume it before its existing reflection implementations.

The design is intentionally Dashboard-specific. It introduces a generic metadata mechanism because the framework cannot contain Dashboard-specific knowledge, but it does not declare general Blazor Native AOT or full-trimming support. Existing applications remain reflection-enabled by default. The generated path is experimental and unsupported outside the Dashboard scenario.

The implementation is organized as six layers: metadata-first serialization, generated JavaScript dispatch, AOT-safe binding, generated application component metadata, framework-owned component metadata, and a strict reflection-disabled proof. The generator ships separately as the prerelease-only `Microsoft.AspNetCore.Components.Endpoints.Generators` analyzer package. Completion requires integrating that package, running the strict proof in CI, and publishing and exercising the pinned Aspire Dashboard revision.

## Goals

**Run the Aspire Dashboard as a Native AOT Interactive Server application.** The Dashboard at `dotnet/aspire` commit `be77aa36daf995fae0e72091141410c7082fcba3` can publish and run natively after its component metadata, JSON contracts, and documented source concessions are supplied. Acceptance covers meaningful interactive Dashboard behavior rather than process startup alone.

**Remove reflection and runtime code generation from the validated Dashboard graph.** Component discovery and execution, interactive form binding, application serialization, JavaScript dispatch, persistence, storage, authorization, QuickGrid, virtualization, and dynamic Server roots use generated metadata. Incomplete metadata is diagnosed at build time where possible and fails explicitly at runtime when reflection is disabled.

**Preserve existing Blazor behavior by default.** Applications that do not register generated metadata continue to use the current reflection paths. Applications that opt in use generated metadata first and retain reflection as a compatibility fallback unless strict validation deliberately disables it.

**Make the proof consumable and durable.** Aspire explicitly references a standalone preview analyzer package rather than an aspnetcore repository project or an automatically delivered targeting-pack analyzer. CI verifies the package remains prerelease-only, publishes and executes the focused surrogate feature matrix, and exercises the pinned Dashboard with all three reflection fallbacks disabled and compiler/ILC warnings treated as errors.

## Non-goals

**General Blazor Native AOT or full-trimming support.** The strict build is a validation bar for the pinned Dashboard graph, not a compatibility promise for every Blazor application or framework feature.

**Arbitrary third-party component compatibility.** Libraries outside the validated Dashboard dependency closure may require their own descriptors, JSON contracts, or source changes; this feature does not guarantee that they work.

**Other hosting models and platforms.** Legacy `AddServerSideBlazor`, Blazor WebAssembly, and Hybrid applications are not part of the supported scenario because the Dashboard uses the Blazor Web Interactive Server path.

**Native AOT HTTP form mapping.** Interactive `EditForm` binding is covered, but server-side HTTP form mapping remains unsupported and is removed only from the strict proof image.

**Templates and customer-facing guidance.** The public metadata contract remains experimental and may be replaced by a broader trimming architecture before it is suitable for templates or general documentation.

## Scenarios

### Register Dashboard metadata

The Dashboard host explicitly references the standalone preview generator package as a private analyzer dependency, declares one partial metadata context, identifies form-model roots and application JSON contracts, and registers the context in dependency injection.

```xml

```

```csharp
[JsonSerializable(typeof(DashboardFilter))]
[JsonSerializable(typeof(DashboardEventArgs))]
internal partial class DashboardJsonContext : JsonSerializerContext;

[BindableModel(ModelType = typeof(SettingsFormModel))]
internal sealed partial class DashboardMetadata : RazorComponentsMetadataContext
{
public override IJsonTypeInfoResolver? JsonTypeInfoResolver
=> DashboardJsonContext.Default;
}

builder.Services.AddComponentMetadata();
builder.Services
.AddRazorComponents()
.AddInteractiveServerComponents();
```

The selected version must contain a NuGet prerelease label. The generator supplies the context's component, binding, and JavaScript descriptor collections without flowing as a transitive dependency or contributing a runtime assembly. Aspire does not reference the generator project directly and does not manually construct descriptors.

### Render and route Dashboard components

Dashboard components live in a Razor class library referenced by the host. Members that generated code must reach are moved to C# code-behind, are at least internal, and avoid unsupported `required` and init-only shapes.

```csharp
public partial class ResourceDetails
{
[Inject]
internal NavigationManager Navigation { get; set; }

[Parameter]
internal string? ResourceName { get; set; }
}
```

The native application discovers routes and endpoint metadata, creates components, resolves keyed and unkeyed injectables, assigns ordinary and cascading parameters, and rerenders after events without reflecting over the component type.

### Bind interactive forms

Each root model reachable from an interactive `EditForm` is named on the metadata context. The generator describes its nested properties, fields, and single-argument indexers.

```csharp
[BindableModel(ModelType = typeof(SettingsFormModel))]
internal sealed partial class DashboardMetadata : RazorComponentsMetadataContext;

internal sealed class SettingsFormModel
{
internal DashboardOptions Options { get; set; }
internal IList Endpoints { get; set; }
}
```

Nested validation expressions, model indexers, arrays, enums, and nullable enums are evaluated without `Expression.Compile` or runtime `MakeGenericMethod`. This scenario concerns interactive component forms, not HTTP form mapping.

### Serialize application state

The Dashboard provides STJ metadata for values crossing component protocol, custom-event, persistence, Session/TempData, and browser-storage boundaries. A storage-only type can instead register a custom protected-storage serializer.

```csharp
[JsonSerializable(typeof(DashboardFilter))]
[JsonSerializable(typeof(DashboardEventArgs))]
[JsonSerializable(typeof(ThemePreference))]
internal partial class DashboardJsonContext : JsonSerializerContext;

builder.Services.AddSingleton<
ProtectedBrowserStorageSerializer,
ThemePreferenceSerializer>();
```

Framework protocol contracts retain their fixed wire formats, application contracts resolve through the registered context, and protected storage preserves the call site's generic type. Missing strict-mode contracts surface normal STJ metadata failures instead of silently enabling reflection.

### Dispatch JavaScript callbacks

The generator discovers accessible public `[JSInvokable]` methods and emits descriptors that know their declared parameter and return types.

```csharp
public static class DashboardInterop
{
[JSInvokable]
public static Task UpdateViewportAsync(
ViewportRequest request);
}
```

Static, instance, inherited, polymorphic, synchronous, `Task`, `Task`, `ValueTask`, and `ValueTask` callbacks deserialize, invoke, await, and serialize through generated code. Existing outbound generic JavaScript calls preserve `TValue` through completion.

### Use framework component providers

The Dashboard uses framework components whose private members and closed generic forms cannot be described safely from application code.

```razor

```

The owning Authorization, QuickGrid, Forms, Web, Endpoints, Media, and WebAssembly Authentication assemblies provide their descriptors. The Dashboard context explicitly roots each required closed generic form through the generator-emitted internal root attribute:

```csharp
[ComponentTypeInfo(typeof(QuickGrid))]
[ComponentTypeInfo(typeof(PropertyColumn))]
[ComponentTypeInfo(typeof(Virtualize))]
internal sealed partial class DashboardMetadata : RazorComponentsMetadataContext;
```

Generated type information then flows through initial, dynamic, and resumed Server roots.

### Publish and validate strictly

The strict lane publishes the focused feature application and pinned Dashboard with Native AOT, warnings as errors, and every relevant reflection fallback disabled.

```xml

true
true
false



```

Required acceptance is a zero-warning publish. The surrogate matrix already proves each framework capability independently; the remaining Dashboard acceptance must prove navigation, interactivity, populated telemetry routes, JavaScript-backed UI, forms, storage, and circuit pause/resume on the real application.

### Preserve the reflection-compatible default

An existing Blazor application does not register a metadata context and does not set strict switches.

```csharp
builder.Services
.AddRazorComponents()
.AddInteractiveServerComponents();
```

Component, binding, JSON, and JavaScript behavior remains unchanged. The framework builds its existing reflection resolvers and the application receives no experimental API diagnostic.

## Design overview

The source generator runs in the Dashboard host compilation and inspects the referenced Razor class library, because source generators cannot consume Razor-generated component types produced in the same compilation. It emits one immutable `RazorComponentsMetadataContext` containing application component descriptors, bindable graphs, JavaScript method descriptors, and the application-provided JSON resolver. `AddComponentMetadata()` takes a host-owned snapshot of that data and installs the internal resolver chains.

**Use explicitly registered aggregate contexts.** A metadata context is the opt-in root for application-owned metadata, and the Dashboard convention is one aggregate context. Multiple contexts can be registered and compose in registration order. This rules out ambient runtime assembly scanning and separate public component, binding, JavaScript, and JSON registries. Explicit registration makes ownership and service-provider isolation visible.

**Resolve generated metadata before reflection, and disable reflection only for proof.** Generated resolvers precede existing reflection resolvers. This rules out changing defaults for existing applications and avoids making Native AOT constraints universal. Three independent switches let the strict lane prove STJ, component metadata, and JavaScript dispatch separately while ordinary applications retain compatibility.

**Generate private framework knowledge in the owning assembly.** Framework packages contribute descriptors for their own hidden members and generic components; the application generator imports those contributions and roots required closures. This rules out broad `DynamicallyAccessedMembers` annotations and an application generator reaching into another assembly's internals. Ownership keeps descriptor drift beside the framework code it describes.

**Compose serialization state per host and runtime.** Framework contracts are first, registered application resolvers follow in registration order, and reflection is appended only when enabled. The resulting options belong to a service provider, renderer, or JavaScript runtime rather than a process-static collection. This rules out cross-host resolver leakage and preserves runtime-bound converters such as JavaScript references.

### Component diagram

```mermaid
graph TD
Package["Preview generator analyzer package"] --> Generator["Blazor metadata generator"]
RazorLibrary["Dashboard Razor class library"] --> Generator
AppRoots["Metadata context + JSON and binding roots"] --> Generator
Generator --> Context["Generated RazorComponentsMetadataContext"]
FrameworkOwners["Framework descriptor providers"] --> Context
Context --> Registration["AddComponentMetadata"]
Registration --> ComponentResolvers["Component and binding resolvers"]
Registration --> JsonResolvers["Host-owned JSON resolver snapshot"]
Registration --> JSResolvers["JS-invokable descriptors"]
ComponentResolvers --> Runtime["Blazor runtime consumers"]
JsonResolvers --> Runtime
JSResolvers --> Runtime
Reflection["Default reflection fallback"] --> Runtime
```

## Public API

All API in this section is new. Metadata and serializer types carry `[Experimental("ASPNETCORE9004")]` unless stated otherwise.

### RazorComponentsMetadataContext and BindableModelAttribute

**Responsibility:** The context is the generated application aggregate. The attribute names a form-model root whose reachable member/indexer graph must be generated.

```csharp
namespace Microsoft.AspNetCore.Components.Web;

public abstract class RazorComponentsMetadataContext
{
protected RazorComponentsMetadataContext();

public abstract IReadOnlyList Components { get; }
public abstract IReadOnlyList BindableTypes { get; }
public abstract IReadOnlyList JSInvokableMethods { get; }
public abstract IJsonTypeInfoResolver? JsonTypeInfoResolver { get; }
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class BindableModelAttribute : Attribute
{
public required Type ModelType { get; init; }
}
```

### ComponentMetadataServiceCollectionExtensions

**Responsibility:** Registers one generated context and installs its component, binding, JavaScript, and JSON metadata in the service provider.

```csharp
namespace Microsoft.Extensions.DependencyInjection;

public static class ComponentMetadataServiceCollectionExtensions
{
public static IServiceCollection AddComponentMetadata(
this IServiceCollection services)
where TContext : RazorComponentsMetadataContext, new();
}
```

### Component descriptors

**Responsibility:** These generated-data contracts describe construction, injectable properties, ordinary and cascading parameters, persistence access, and attribute-shaped component metadata.

```csharp
namespace Microsoft.AspNetCore.Components.Infrastructure;

public sealed class ComponentDescriptor
{
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public required Type Type { get; init; }
public Func? CreateInstance { get; init; }
public IReadOnlyList Parameters { get; init; }
public IReadOnlyList Injectables { get; init; }
public IReadOnlyList Metadata { get; init; }
}

public sealed class ComponentParameterDescriptor
{
public required string Name { get; init; }
public required Type ParameterType { get; init; }
public required Attribute Attribute { get; init; }
public required Action SetValue { get; init; }
public required Func GetValue { get; init; }
public Func? GetStateSerializer { get; init; }
}

public sealed class ComponentInjectableDescriptor
{
public required string Name { get; init; }
public required Type ServiceType { get; init; }
public required InjectAttribute Attribute { get; init; }
public required Action SetValue { get; init; }
}
```

### Bindable descriptors

**Responsibility:** These generated-data contracts let interactive form expressions traverse a model graph without compiling expression trees or reflecting over each hop.

```csharp
namespace Microsoft.AspNetCore.Components.Infrastructure;

public sealed class BindableTypeDescriptor
{
public required Type Type { get; init; }
public IReadOnlyList Members { get; init; }
public IReadOnlyList Indexers { get; init; }
}

public sealed class BindableMemberDescriptor
{
public required string Name { get; init; }
public required Type MemberType { get; init; }
public required Func GetValue { get; init; }
}

public sealed class BindableIndexerDescriptor
{
public required Type IndexType { get; init; }
public required Type ValueType { get; init; }
public required Func GetValue { get; init; }
}
```

### JSInvokableMethodDescriptor and JSInvokableMethodKind

**Responsibility:** A descriptor owns the complete incoming JavaScript call for one method because only generated code statically knows every parameter and return type. The kind preserves inheritance and explicit override-blocking semantics.

```csharp
namespace Microsoft.JSInterop.Infrastructure;

public sealed class JSInvokableMethodDescriptor
{
public required string AssemblyName { get; init; }
public required Type TargetType { get; init; }
public required string Identifier { get; init; }
public required bool IsStatic { get; init; }
public string? MethodKey { get; init; }
public JSInvokableMethodKind Kind { get; init; }
public required Func>
Invoke { get; init; }
}

public enum JSInvokableMethodKind
{
Method = 0,
Override = 1,
OverrideBlocker = 2,
}
```

### Modified JSRuntime

**Change:** A runtime can expose generated method descriptors to the dispatcher. The default is `null`, so every existing runtime remains reflection-only.

```diff
namespace Microsoft.JSInterop;

public abstract partial class JSRuntime
{
+ protected internal virtual
+ IReadOnlyList? InvokableMethods { get; }
}
```

**Behavior changes:** When descriptors are present they resolve before reflection. Duplicate generated identifiers fail during resolver construction.

### ProtectedBrowserStorageSerializer and generic SetAsync

**Responsibility:** The serializer is the explicit escape hatch for storage-only values that do not have a JSON contract. Generic writes retain the compile-time type so the registered serializer or generated JSON contract can be selected.

```csharp
namespace Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;

public abstract class ProtectedBrowserStorageSerializer
{
public abstract string Serialize(T value);
public abstract T Deserialize(string data);
}
```

The following overloads are non-experimental. Overload resolution can select them for existing calls; with no custom serializer they preserve the existing JSON behavior.

```diff
public abstract class ProtectedBrowserStorage
{
+ public ValueTask SetAsync(string key, TValue value);
+ public ValueTask SetAsync(string purpose, string key, TValue value);
}
```

## Detailed design

### Internal primitives

#### RazorComponentsMetadataGenerator

**Responsibility:** Finds partial metadata contexts, inspects referenced application and framework metadata, validates complete descriptor generation, and emits immutable context data. It reads `RazorComponentsReflectionEnabledByDefault` as a compiler-visible property so strict builds promote reflection-dependent component omissions to errors.

```csharp
public sealed partial class RazorComponentsMetadataGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context);
}
```

The generator class is public only because Roslyn instantiates it from the analyzer assembly; it is not a product consumer API. The project targets `netstandard2.0`, is packable as `Microsoft.AspNetCore.Components.Endpoints.Generators`, and places only the generator assembly under `analyzers/dotnet/cs`. It contributes no `lib`, `ref`, runtime, build, or transitive runtime asset. Consumers add an explicit `PackageReference` with `PrivateAssets="all"`.

The package version is always prerelease. The project uses Arcade's preview-only pattern: `TreatAsLocalProperty="PreReleaseVersionLabel;PreReleaseVersionIteration"`, `SuppressFinalPackageVersion=true`, and the repository's `PreviewOnlyPackagePreReleaseVersionLabel` and `PreviewOnlyPackagePreReleaseVersionIteration` values. A local final-version override simulation still produced `Microsoft.AspNetCore.Components.Endpoints.Generators.11.0.0-dev.nupkg`, and the package test independently rejects any produced nuspec version without `-`. The simulation is not a substitute for an official Arcade RC/RTM pipeline build, which remains release-pipeline validation.

The strict sample restores the locally built package from `ArtifactsShippingPackagesDir` with a prerelease version range, while the E2E harness discovers and passes the exact packed version into nested publish. External compilation exposed generator-owned `BL0005` and inherited nullable generic-base `CS8620` diagnostics; emitted code suppresses those diagnostics narrowly, with a focused regression test.

#### ComponentTypeInfoAttribute

**Responsibility:** The generator emits this internal attribute into the host compilation so application source can explicitly root a closed generic component that cannot be discovered as an ordinary concrete component type.

```csharp
namespace Microsoft.AspNetCore.Components.Web;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal sealed class ComponentTypeInfoAttribute : Attribute
{
public ComponentTypeInfoAttribute(Type componentType);
}
```

#### ComponentMetadataResolver

**Responsibility:** Captures registered `ComponentDescriptor` instances in registration order and provides both enumeration and type lookup. Descriptor merging and deduplication occur when the source-generated type-info resolver builds its caches.

```csharp
internal sealed class ComponentMetadataResolver : IComponentMetadataResolver
{
public IReadOnlyList Components { get; }
public bool TryGetComponentDescriptor(
Type type,
out ComponentDescriptor? descriptor);
}
```

#### ComponentTypeInfo

**Responsibility:** Normalizes generated and reflective component facts into the internal shape already consumed by activation, injection, parameter assignment, discovery, routing, persistence, and root-component operations.

```csharp
internal sealed class ComponentTypeInfo
{
internal Type Type { get; }
internal Func? CreateInstance { get; }
internal IReadOnlyList Parameters { get; }
internal IReadOnlyList Injectables { get; }
internal IReadOnlyList Metadata { get; }
internal ComponentDescriptor Descriptor { get; }
}
```

#### SourceGeneratedComponentTypeInfoResolver

**Responsibility:** Converts complete public descriptors to `ComponentTypeInfo`, merges framework and application contributions, and returns no result when a component cannot be described completely.

```csharp
internal sealed class SourceGeneratedComponentTypeInfoResolver
: IComponentTypeInfoResolver
{
public ComponentTypeInfo? GetTypeInfo(Type componentType);
public ComponentTypeInfo? GetTypeInfo(string assemblyName, string typeName);
public IReadOnlyList GetTypeInfos(Assembly assembly);
}
```

`ComponentTypeInfoResolverFactory` places this resolver before the existing reflection resolver. When component reflection is disabled, an unresolved type or missing activation factory throws `NotSupportedException`. Custom component and property activators are rejected in strict mode because the framework cannot prove their metadata behavior.

#### ComponentBindableTypeResolver

**Responsibility:** Flattens all registered metadata contexts into a type-keyed lookup used by binding expression evaluation.

```csharp
internal sealed class ComponentBindableTypeResolver : IBindableTypeResolver
{
public bool TryGetBindableTypeDescriptor(
Type type,
out BindableTypeDescriptor? descriptor);
}
```

#### ComponentJsonMetadataResolver

**Responsibility:** Captures application `IJsonTypeInfoResolver` instances in registration order for the current service provider.

```csharp
internal sealed class ComponentJsonMetadataResolver : IComponentJsonMetadataResolver
{
public IJsonTypeInfoResolver? JsonTypeInfoResolver { get; }
}
```

The resolver combines the registered application resolvers in order into one host-owned resolver. Framework serializer options place protocol-owned generated contracts before that combined resolver and reflection after it. A provider snapshot is immutable after creation, preventing metadata registered for one host from leaking into another.

#### SourceGeneratedJSInvokableMethodResolver

**Responsibility:** Resolves generated JavaScript methods by assembly and identifier for static calls or receiver type and identifier for instance calls.

```csharp
internal sealed class SourceGeneratedJSInvokableMethodResolver
: IJSInvokableMethodResolver
{
public bool TryResolve(
in JSInvokableMethodInfo methodInfo,
out JSInvokableMethodDescriptor? descriptor);
}

internal readonly record struct JSInvokableMethodInfo(
string? AssemblyName,
Type? TargetType,
string Identifier);
```

`JSInvokableMethodResolverFactory` places it before the reflection resolver. A descriptor receives the runtime's `JsonSerializerOptions`, which contain converters bound to that runtime or circuit, rather than capturing shared options.

#### BuiltInComponentDescriptors

**Responsibility:** Each framework assembly describes the components and closed-generic factories it owns, including inaccessible members the application generator cannot safely reach.

```csharp
internal static class BuiltInComponentDescriptors
{
internal static ComponentDescriptor[] GetDescriptors();
}
```

The generator imports these providers through generated internal accessors. Components, Web, Forms, Endpoints, Authorization, QuickGrid, Media, and WebAssembly Authentication own their contributions. Duplicate type/member contributions are coalesced deterministically.

### Dependency map

```mermaid
classDiagram
RazorComponentsMetadataGenerator --> RazorComponentsMetadataContext : emits
RazorComponentsMetadataContext --> ComponentDescriptor
RazorComponentsMetadataContext --> BindableTypeDescriptor
RazorComponentsMetadataContext --> JSInvokableMethodDescriptor
RazorComponentsMetadataContext --> IJsonTypeInfoResolver
ComponentMetadataServiceCollectionExtensions --> ComponentMetadataResolver
ComponentMetadataServiceCollectionExtensions --> ComponentBindableTypeResolver
ComponentMetadataServiceCollectionExtensions --> ComponentJsonMetadataResolver
SourceGeneratedComponentTypeInfoResolver --> ComponentMetadataResolver
SourceGeneratedComponentTypeInfoResolver --> ComponentTypeInfo
JSRuntime --> SourceGeneratedJSInvokableMethodResolver
BuiltInComponentDescriptors --> ComponentDescriptor
```

### Generator discovery and diagnostics

The host and component library are separate compilations. Razor-generated component types exist in the referenced library's metadata when the host generator runs; they are not available to a generator running beside Razor in the same compilation. The Dashboard therefore keeps components in its Razor class library and the partial context in its host.

The generator includes public component types reachable from the referenced assembly set and explicit closed-generic roots required by generated framework providers. It reconstructs route, layout, render-mode, authorization, caching, and other endpoint-visible attributes. It emits direct access for visible members and generated accessors where the supported visibility rules permit them. A component is described only when the framework-required shape is complete; it never emits a partial descriptor that silently changes semantics.

Diagnostics are:

- `BLAZORAOT001`: a component cannot be described completely; warning normally and error when component reflection is disabled.
- `BLAZORAOT002`: a bindable model graph cannot be described completely.
- `BLAZORAOT003`: the metadata context or containing type is not partial; always an error.
- `BLAZORAOT004`: an endpoint-visible component attribute cannot be reconstructed; warning normally and error when component reflection is disabled.

The Dashboard removes or reshapes `required` and init-only component members because this iteration does not generate constructor bypasses or init-only setter accessors. Metadata-relevant members move to ordinary C# code-behind and are at least internal so the generator can name them consistently.

### Runtime component flow

The generated resolver is the common source for component discovery and execution. `ComponentApplicationBuilder` obtains descriptors for endpoint discovery. `RouteTableFactory` reads route and layout metadata. `DefaultComponentActivator` uses the generated factory. `ComponentFactory` applies injectable properties. Parameter and cascading-value assignment use generated setters, while persistent state uses generated getters and optional typed serializer accessors.

Initial Server roots carry `ComponentTypeInfo` from discovery into circuit creation. Dynamic JavaScript roots and resumed roots resolve and retain the same type information. Registered WebAssembly root mappings are materialized before manually added roots to preserve the existing ordering contract even though WebAssembly itself is not an acceptance platform.

### Binding flow

`BindingExpressionEvaluator` first anchors an expression at the node whose static type matches the `EditContext` model. It walks subsequent property, field, and indexer hops through `BindableTypeDescriptor`. Arrays are indexed directly. If generated metadata is unavailable and reflection remains enabled, the existing member walk runs.

`BindConverter` uses non-generic enum and nullable-enum parsing and reflectionless array conversion, so it does not close helper methods with `MakeGenericMethod` at runtime. Generated bindable metadata covers interactive forms only. `FormDataMapper` remains the independent HTTP form-mapping subsystem and is not made AOT-safe by this work.

### Serialization flow

Every serialization purpose begins with framework-generated fixed contracts. Marker payloads, prerender state, initializer responses, root-operation batches, renderer identifiers, navigation options, browser-file callbacks, and render fragments therefore retain their established names and null-handling policies.

Application contexts compose through `ComponentJsonMetadataResolver` in registration order. The framework obtains a contract through the protocol-owned `JsonSerializerOptions` rather than serializing directly with a context's `JsonTypeInfo`, because direct context use can bypass camel-case and null-omission policies. Reflection is appended only when `JsonSerializer.IsReflectionEnabledByDefault` is true.

Persistent component state, Session, TempData, protected browser storage, WebAssembly component parameters, and Server marker payloads all consult the same application metadata source while retaining purpose-specific framework contracts. JavaScript runtime options additionally contain converters for `ElementReference`, JavaScript references and streams, transferred byte arrays, `DotNetStreamReference`, and `DotNetObjectReference`.

### JavaScript dispatch flow

The generator emits only accessible public `[JSInvokable]` methods. Each descriptor owns argument deserialization, invocation, asynchronous result normalization, and result serialization. This avoids moving reflection from method lookup into serializer calls over runtime `Type` values.

Static lookup uses `(AssemblyName, Identifier)`. Instance lookup walks the receiver's base-type chain using `(TargetType, Identifier)`. `JSInvokableMethodKind.Override` preserves generated inheritance, while `OverrideBlocker` prevents an unannotated override from accidentally exposing an annotated base method. Duplicate generated identifiers throw during resolver construction; an unresolved call retains the existing argument error.

### Protected browser storage

The generic `SetAsync` overloads retain the call site's static type. A registered `ProtectedBrowserStorageSerializer` takes precedence. Otherwise storage uses application JSON metadata and appends reflection only when STJ reflection is enabled. Reads remain generic and use the same selection.

The overloads are stable additions because overload resolution can select them for existing source. Their default behavior is intentionally identical to the object overloads. The custom serializer type is experimental because implementing it is an explicit opt-in to the new extension point.

### Strict validation and acceptance

`BlazorAotStrictMode=true` flows from the E2E project into the published sample. It disables all three reflection switches and enables a strict-only ILC substitution for private HTTP form-mapping methods that are unsupported and unexercised. The substitution is doubly conditioned on strict mode and Native AOT publish. It is not product behavior and cannot be used as evidence of form-mapping support.

The focused regression suite has passed 21 strict Native AOT browser scenarios, 22 JIT generated-metadata scenarios, and one JIT reflection-default witness locally. Native publish reports zero compiler and ILC warnings. Required completion work adds the same strict invocation as an ordinary CI leg rather than leaving it as an undocumented local command.

Real acceptance starts from an unpushed validation branch that layers commit `c18c367980` (`Add the Aspire Dashboard as a Native AOT test asset`) on top of the final distributed stack. Package-based validation removed its direct metadata/RDG analyzer references, restored `Microsoft.AspNetCore.Components.Endpoints.Generators` version `11.0.0-dev`, confirmed the analyzer asset in `project.assets.json`, and enabled the strict switches, analyzers, and warning gates. Strict publish stopped before E2E execution on 89 analysis errors: 32 IL2026, 4 IL2075, 1 IL2091, 5 IL2110, 9 IL2111, and 38 IL3050. The stale preview.7 ILCompiler also crashed on an unsafe-accessor generic constraint generated for `DesignToken`. None of the five scaffold tests ran. Required completion refreshes the scaffold from Aspire `173a2109e` to `be77aa36daf995fae0e72091141410c7082fcba3` and the current toolchain, resolves the consuming-library diagnostics, and expands the browser/OTLP tests to the claimed Dashboard graph. The temporary Dashboard/Fluent UI sources remain validation input and are never included in the shipping PRs.

### Scenario walkthroughs

#### Register Dashboard metadata

**Participants:** preview generator package, `RazorComponentsMetadataGenerator`, `DashboardMetadata`, `AddComponentMetadata()`.

```mermaid
sequenceDiagram
participant Build
participant Generator
participant Context
participant DI
Build->>Generator: Compile host with referenced Razor library
Generator->>Context: Emit immutable metadata collections
DI->>Context: Construct registered context
DI->>DI: Snapshot component, binding, JS, and JSON metadata
```

**Walkthrough:** NuGet restores the explicit private analyzer package and Roslyn loads its analyzer-only asset. The host's partial context and referenced component metadata are generator inputs. Registration creates one context instance and copies its contributions into service-provider-owned options before runtime consumers resolve.

#### Render and route Dashboard components

**Participants:** `ComponentApplicationBuilder`, `SourceGeneratedComponentTypeInfoResolver`, `ComponentTypeInfo`, `ComponentFactory`.

```mermaid
sequenceDiagram
participant Discovery
participant Resolver
participant Factory
participant Component
Discovery->>Resolver: Resolve component type
Resolver-->>Discovery: ComponentTypeInfo
Factory->>Component: Construct, inject, and assign parameters
Component-->>Factory: Rendered instance
```

**Walkthrough:** Discovery and execution consume the same normalized type info, so routing cannot preserve a type that activation later loses. Generated attribute instances drive route and endpoint policy. The factory invokes compile-time delegates for construction, injection, and parameter assignment.

#### Bind interactive forms

**Participants:** `EditContext`, `BindingExpressionEvaluator`, `ComponentBindableTypeResolver`, `BindConverter`.

```mermaid
sequenceDiagram
participant Form
participant Evaluator
participant Resolver
Form->>Evaluator: Evaluate field expression
Evaluator->>Resolver: Resolve each model type
Resolver-->>Evaluator: Member and indexer getters
Evaluator-->>Form: FieldIdentifier and converted value
```

**Walkthrough:** The evaluator anchors at the edit model and follows generated accessors. Conversion handles arrays and enum forms without runtime generic closure. Missing generated entries use reflection only in the default mode.

#### Serialize application state

**Participants:** purpose-specific serializer options, framework contexts, `ComponentJsonMetadataResolver`, optional storage serializer.

```mermaid
sequenceDiagram
participant Caller
participant Options
participant Framework
participant Application
Caller->>Options: Resolve contract
Options->>Framework: Try fixed framework metadata
Options->>Application: Try registered application metadata
Options-->>Caller: Contract or explicit metadata error
```

**Walkthrough:** Framework wire contracts cannot be shadowed by application contexts. Application contexts resolve Dashboard values next. Reflection participates only in the compatibility mode; storage can bypass JSON through a typed custom serializer.

#### Dispatch JavaScript callbacks

**Participants:** `JSRuntime`, composite resolver, generated descriptor, runtime JSON options.

```mermaid
sequenceDiagram
participant JavaScript
participant Runtime
participant Resolver
participant Descriptor
JavaScript->>Runtime: Invoke identifier with JSON arguments
Runtime->>Resolver: Resolve generated method
Resolver->>Descriptor: Invoke receiver and payload
Descriptor-->>JavaScript: Awaited JSON result
```

**Walkthrough:** Lookup is generated-first. The descriptor uses statically known generic serializer calls and runtime-bound options, invokes the method, normalizes every supported return shape, and returns the serialized result.

#### Use framework component providers

**Participants:** owning framework assemblies, generated provider accessors, application context, component resolver.

```mermaid
sequenceDiagram
participant Owner
participant Generator
participant Context
participant Resolver
Owner->>Generator: Expose built-in descriptor provider
Generator->>Context: Import provider and generic roots
Context->>Resolver: Register merged descriptors
Resolver-->>Resolver: Deduplicate by type and member
```

**Walkthrough:** The owner creates descriptors for private and generic implementation details. The application generator only connects providers and closes app-specific generic forms. Deterministic deduplication allows multiple registered contexts without duplicate runtime metadata.

#### Publish and validate strictly

**Participants:** CI, Native AOT compiler, strict sample, pinned Dashboard.

```mermaid
sequenceDiagram
participant CI
participant ILC
participant Sample
participant Dashboard
CI->>ILC: Publish with reflection disabled and warnings as errors
ILC-->>CI: Native binaries with zero warnings
CI->>Sample: Execute focused browser matrix
CI->>Dashboard: Execute real interactive acceptance
```

**Walkthrough:** The sample isolates regressions by framework capability. The Dashboard proves that the same product-delivered generator and runtime cover the actual dependency closure. Either warning, missing metadata failure, browser error UI, or acceptance failure blocks completion.

#### Preserve the reflection-compatible default

**Participants:** ordinary application, resolver factories, existing reflection resolvers.

```mermaid
sequenceDiagram
participant App
participant Factory
participant Reflection
App->>Factory: Start without generated context
Factory->>Reflection: Build default resolver chains
Reflection-->>App: Existing behavior
```

**Walkthrough:** No generated context is required and no strict switch changes. The new factories append the same reflection implementations used before this feature, preserving source and runtime compatibility.

### Design decisions

#### ADR-1: Ship the generator as an explicit preview analyzer package

| | |
|---|---|
| Status | Accepted; implemented on follow-up branch |
| Context | Aspire cannot reference an aspnetcore repository project and the framework metadata contract is unusable without generated implementations. |
| Alternatives considered | Automatic targeting-pack delivery hides the experimental dependency and makes it appear stable; a repo-local project reference proves tests only; embedding generation in runtime assemblies is impossible. |
| Decision | Pack `Microsoft.AspNetCore.Components.Endpoints.Generators` as an analyzer-only NuGet package. Aspire references an explicit prerelease version with `PrivateAssets="all"`. Arcade preview-only properties force a prerelease result even for final-version builds. |
| Rationale | Explicit acquisition makes preview status and unsupported scope visible while still removing repository coupling. |
| Consequences | Aspire must update the package version deliberately; the package never publishes a stable release and contributes no runtime asset. |

#### ADR-2: Emit explicitly registered aggregate metadata contexts

| | |
|---|---|
| Status | Accepted |
| Context | The runtime needs component, binding, JavaScript, and JSON metadata from the same application. |
| Alternatives considered | Separate registries expose more API and scatter setup; runtime assembly scanning preserves reflection and mutable global state. |
| Decision | Generate an aggregate `RazorComponentsMetadataContext` per declared partial context and register each explicitly; the Dashboard uses one by convention. |
| Rationale | Each context is a cohesive generated-code contract, while ordered multi-context composition supports independently supplied application metadata. |
| Consequences | Contexts are STJ-aware in this iteration and are not the final serializer-neutral architecture. |

#### ADR-3: Treat generated component descriptors as complete units

| | |
|---|---|
| Status | Accepted |
| Context | Partially generated activation or parameter metadata can silently change component behavior. |
| Alternatives considered | Merge each available generated member with reflective omissions; emit incomplete descriptors and fail only when used. |
| Decision | Emit a component descriptor only when required behavior can be represented; diagnose omissions. |
| Rationale | A descriptor is authoritative and deterministic, and strict builds fail before an affected component is exercised. |
| Consequences | Unsupported source shapes require Dashboard changes or remain reflection-dependent. |

#### ADR-4: Keep generated-first, reflection-last compatibility

| | |
|---|---|
| Status | Accepted |
| Context | Existing applications rely on reflection while strict validation must prove its absence. |
| Alternatives considered | Disable reflection for every published app; require a new runtime mode; retain reflection without a proof switch. |
| Decision | Preserve reflection defaults and expose three runtime configuration switches used by strict validation. |
| Rationale | Compatibility and proof are independent concerns. |
| Consequences | Native AOT support is only demonstrated when CI explicitly enables strict mode. |

#### ADR-5: Let a JavaScript descriptor own the whole call

| | |
|---|---|
| Status | Accepted |
| Context | Lookup alone does not remove reflection because argument and result types are otherwise held as runtime `Type` values. |
| Alternatives considered | Generate only a method delegate; use untyped STJ APIs after generated lookup. |
| Decision | Generate deserialize, invoke, await, and serialize as one descriptor delegate. |
| Rationale | Generated code statically names every type and preserves existing wire behavior. |
| Consequences | Descriptors receive runtime options to avoid sharing circuit-bound converters. |

#### ADR-6: Put framework descriptors with their owners

| | |
|---|---|
| Status | Accepted |
| Context | Application generation cannot safely access private framework members and open generic component internals. |
| Alternatives considered | Make framework implementation public; root all members dynamically; duplicate framework knowledge in the application generator. |
| Decision | Each framework assembly owns built-in descriptors and the app generator imports them. |
| Rationale | It preserves package boundaries and keeps metadata synchronized with implementation. |
| Consequences | Adding or changing a framework component may require updating its provider and tests. |

#### ADR-7: Scope JSON resolver composition to the host

| | |
|---|---|
| Status | Accepted |
| Context | Process-static resolver accumulation leaks contracts across independently configured hosts and tests. |
| Alternatives considered | A global resolver list; rebuilding contexts against shared mutable options. |
| Decision | Capture immutable resolver lists per service provider and materialize runtime-specific options where needed. |
| Rationale | This matches DI ownership and circuit-bound converter lifetimes. |
| Consequences | Each serialization purpose still owns its framework-first options composition. |

#### ADR-8: Exclude HTTP form mapping from strict proof

| | |
|---|---|
| Status | Accepted |
| Context | Endpoints reference Native AOT-unsafe HTTP form mapping that the Interactive Server matrix does not exercise. |
| Alternatives considered | Claim and implement form-mapping support; broadly suppress ILC warnings; remove product code. |
| Decision | Apply a doubly conditioned strict-test-only ILC substitution and state the non-goal. |
| Rationale | The proof image contains only validated behavior without hiding unrelated warnings. |
| Consequences | Applications requiring HTTP form mapping remain unsupported by this feature. |

#### ADR-9: Require both focused and real-Dashboard acceptance

| | |
|---|---|
| Status | Accepted; implementation blocked |
| Context | The surrogate matrix diagnoses framework regressions but does not prove the actual Aspire dependency closure. |
| Alternatives considered | Treat startup as sufficient; validate only the surrogate; vendor the entire Dashboard permanently into the shipping stack. |
| Decision | Keep the focused matrix and use `c18c367980` as an unpushed scaffold, then refresh it to the pinned Dashboard, distributed generator, strict settings, and full claimed acceptance graph. |
| Rationale | Layered tests provide both actionable failures and proof of the stated user outcome without permanently vendoring Dashboard sources. |
| Consequences | Validation is reproducible from a commit, while roughly 1,400 vendored Dashboard/Fluent UI files stay out of the distributed stack. |

### Design review evaluation

#### Design principles

| Rule | Applies to | Verdict | Notes |
|---|---|---|---|
| Cover every scenario | Entire design | Pass | Eight scenarios have corresponding internal walkthroughs and validation. |
| Respect existing architecture | Generator delivery, DI, resolver chains | Pass | Uses the standard analyzer-only NuGet layout, options/DI snapshots, existing component interfaces, and STJ resolver composition. |
| Cohesion and coupling | Context and descriptors | Pass with risk | Each descriptor is cohesive; the aggregate context intentionally couples four metadata domains to minimize public registration API. |
| Consistent abstraction levels | Public descriptors and internal resolvers | Pass | Public types are data contracts; orchestration stays internal. |
| Working-memory limits | All public types | Pass | No method exceeds four parameters and no type exceeds seven public members. |
| Information hiding | Framework providers and resolvers | Pass | Runtime providers, options, feature switches, and normalized type info remain internal. |

#### API surface

| Rule | Applies to | Verdict | Notes |
|---|---|---|---|
| No method over four parameters | Public and internal methods | Pass | The largest public delegate has three parameters. |
| No boolean behavior switch | Public API | Pass | Strict switches are runtime configuration, not public method parameters. |
| Clear names and effects | Metadata descriptors, registration | Pass with risk | Names match roles, though `RazorComponentsMetadataContext` is broader than a serialization context. |
| No query-and-mutate ambiguity | Resolvers and registration | Pass | Resolvers query immutable snapshots; registration mutates DI explicitly. |
| Familiar names preserve semantics | Metadata context and serializer | Pass | STJ resolver semantics and `SetAsync` behavior are retained. |

#### Type structure

| Rule | Applies to | Verdict | Notes |
|---|---|---|---|
| At most seven public members | All new types | Pass | `JSInvokableMethodDescriptor` has seven; all others have fewer. |
| Single responsibility | Descriptor types | Pass | Component parameter, injectable, binding, and JS roles are separated. |
| Extensible without editing a central switch | Framework providers, JSON resolvers | Pass | New owner providers and contexts compose as contributions. |
| Discoverable correct usage | Metadata context and registration | Pass with risk | The analyzer supplies diagnostics; experimental documentation must explain the host/library split. |
| Invalid states prevented | Descriptor initialization | Pass with risk | Required members prevent omissions in generated code; semantic combinations are validated by the generator/runtime. |

#### Package and namespace

| Rule | Applies to | Verdict | Notes |
|---|---|---|---|
| Namespace size remains navigable | `Components.Infrastructure`, `JSInterop.Infrastructure` | Pass | Generated-code contracts are grouped in existing infrastructure namespaces. |
| No cyclic package dependency | Framework providers and context | Pass | Owner providers flow upward through generator-produced data without runtime package cycles. |
| No cross-package internal reach | Provider import | Pass | Generated accessors are emitted deliberately; runtime packages do not reference another package's internals. |

#### Feature and framework consistency

| Rule | Applies to | Verdict | Notes |
|---|---|---|---|
| Similar operations use consistent conventions | Component, binding, JS, JSON resolution | Pass | Each uses ordered generated-first resolution with explicit fallback. |
| Setup is not scattered | Consumer opt-in | Pass | Context declaration and one DI call are the two setup locations. |
| Same role uses same name | Descriptor/provider/resolver naming | Pass | Public data uses `Descriptor`; internal lookup uses `Resolver`; framework contributions use providers. |
| Required knowledge is documented | App concessions and strict mode | Pass | Scenarios and detailed design state compilation boundaries, roots, JSON contracts, and unsupported shapes. |
| Extension models are consistent | JSON and framework contributions | Pass with risk | JSON follows native STJ composition; component/JS contracts are feature-specific experimental models. |

### Assumptions

- The preview package is published to a feed available to every supported Aspire build environment and its version matches the experimental framework API it generates against. Package restore, external-consumer compilation, and the pinned Dashboard build validate this before completion.
- Aspire can retain its host/Razor-library split so the host generator sees compiled component metadata. The pinned revision is built through this arrangement in acceptance.
- Dashboard component members can be moved to code-behind, made at least internal, and stripped of unsupported `required`/init-only modifiers without changing user-visible behavior. Source diff review and Dashboard tests validate this.
- The pinned Dashboard dependency closure uses only the framework and third-party component shapes covered by generated descriptors. Strict publish diagnostics and interactive acceptance validate the closure.
- Framework protocol formats remain stable when generated contracts resolve through protocol-owned options. Existing protocol tests and strict browser tests validate byte/JSON behavior.
- The strict ILC substitution matches only private HTTP form-mapping implementation in the test image. Its project conditions and binary inspection prevent it from affecting product or non-strict builds.

### Risks and open questions

- **Standalone package delivery is implemented but not integrated into the distributed stack.** The follow-up branch has green analyzer-only package, prerelease-policy, external-consumer, non-transitive dependency, generator, and strict sample validation. Its seven-file change still requires review and placement in the appropriate stack layer.
- **Real Dashboard strict acceptance is blocked before E2E execution.** The explicit package reaches the temporary `c18c367980` overlay, but 89 trim/AOT analysis errors and a stale preview.7 ILCompiler unsafe-accessor constraint crash stop publish. The pinned Dashboard lane on the current toolchain still must prove this exact generator/runtime stack, not the separate minimal experiment.
- **Referenced-assembly scanning can diagnose unused components.** A third-party assembly in the Dashboard closure may contain unsupported components the Dashboard never instantiates. The generator must define deterministic inclusion/rooting rules or the adaptation must isolate the required closure.
- **Framework provider drift is manual.** Private member or generic component changes can invalidate an owner descriptor. Provider generator tests and strict scenario coverage reduce but do not eliminate this risk.
- **The public shape is transitional.** A future serializer-neutral library/model-provider architecture may replace the aggregate context and descriptors. Experimental annotations make that change possible, but generated consumers and Aspire adaptations will need coordinated updates.
- **Unsupported component member shapes constrain Aspire source.** `required` and init-only support is deliberately absent. Reintroducing those shapes requires a separate design for construction and setter accessors.
- **The stable generic storage overloads broaden API permanently.** Their behavior is compatible, but overload-resolution and source-compatibility tests must remain because they cannot be withdrawn with the experimental surface.

### References

- [API proposal #68169: Aspire Dashboard AoT framework support](https://github.com/dotnet/aspnetcore/issues/68169)
- [PR #68295: Make component serialization metadata-first](https://github.com/dotnet/aspnetcore/pull/68295)
- [PR #68296: Generate JS-invokable dispatch metadata](https://github.com/dotnet/aspnetcore/pull/68296)
- [PR #68297: Generate AOT-safe binding metadata](https://github.com/dotnet/aspnetcore/pull/68297)
- [PR #68299: Generate component metadata for Native AOT](https://github.com/dotnet/aspnetcore/pull/68299)
- [PR #68300: Generate framework component metadata](https://github.com/dotnet/aspnetcore/pull/68300)
- [PR #68302: Validate component metadata under Native AOT](https://github.com/dotnet/aspnetcore/pull/68302)
- [GitHub stack #68304](https://github.com/dotnet/aspnetcore/pulls/68304)
- [Final six-PR stack tip `72744044d20274e429f38d40e49c0d5ceed24391`](https://github.com/dotnet/aspnetcore/commit/72744044d20274e429f38d40e49c0d5ceed24391)
- [`dotnet/aspire` acceptance revision `be77aa36daf995fae0e72091141410c7082fcba3`](https://github.com/dotnet/aspire/commit/be77aa36daf995fae0e72091141410c7082fcba3)
- Validation scaffold commit `c18c367980`: `Add the Aspire Dashboard as a Native AOT test asset`
- Scaffold-removal commit `915a526651`: `Revert "Add the Aspire Dashboard as a Native AOT test asset"`

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the six implementation layers, the standalone Microsoft.AspNetCore.Components.Endpoints.Generators analyzer package, and the pinned Aspire Dashboard revision. Done means a zero-warning Native AOT publish with reflection fallbacks disabled, CI coverage of the strict proof, and exercised Dashboard navigation, interactivity, forms, storage, JavaScript-backed UI, telemetry routes, and circuit pause/resume.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend, web-dev
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.