[Blazor] Generate JS-invokable dispatch metadata - #68296
Open
javiercn wants to merge 12 commits into
Open
Conversation
javiercn
force-pushed
the
javiercn-aot-stack-2-jsinterop
branch
2 times, most recently
from
August 9, 2026 12:28
e53c2c0 to
08239d6
Compare
Open
3 tasks
javiercn
force-pushed
the
javiercn-aot-stack-2-jsinterop
branch
from
August 9, 2026 19:17
08239d6 to
bdf3e2a
Compare
3 tasks
Keep each outbound call's generic result type alive until completion so result deserialization no longer reconstructs it from runtime Type values. Behavior: preserved State: builds; parity coverage follows in the next commit Review hint: the pending-call abstraction replaces TaskCompletionSource reflection without changing completion, cancellation, or failure flow. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise typed completion for nullable outbound JS results in addition to the existing object, array, failure, and cancellation coverage. Behavior: preserved State: complete for typed pending-call completion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move method scanning, argument parsing, invocation, async adaptation, and caches behind an internal reflection resolver so DotNetDispatcher only coordinates wire dispatch. Behavior: preserved State: builds; generated resolution follows in the next commit Review hint: the resolver contains the legacy reflection implementation and its suppressions; DotNetDispatcher now handles only protocol flow. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Expose executable JS-invokable descriptors and compose generated lookup ahead of an independently switchable reflection compatibility resolver. Behavior: changed: runtimes can supply generated JS-invokable descriptors that take precedence over reflection State: builds; dispatch matrix coverage follows in the next commit Review hint: SourceGeneratedJSInvokableMethodResolver defines precedence and inheritance; the factory keeps reflection enabled by default behind its own linker-recognized switch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cover manual generated descriptors, generated-first precedence, inheritance and alias rules, misses, reflection parity, async completion, and object-reference disposal. Behavior: preserved State: complete for runtime descriptor resolution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add the JS-only metadata generator foundation, extend the experimental application context with JSInvokableMethods, and let RemoteJSRuntime flatten registered contexts into generated-first dispatch. Behavior: changed: registered application metadata contexts now contribute generated JS-invokable methods State: builds; generator and runtime integration tests follow in the next commit Review hint: this layer emits only JsonTypeInfoResolver and JSInvokableMethods; component and bindable collection/emission are intentionally absent. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise generated JS descriptors across method shapes, aliases, inheritance, and serialization, and verify registered contexts reach RemoteJSRuntime in registration order. Behavior: preserved State: complete for application-generated JS metadata Review hint: the generator test project contains only the JS slice; component and bindable generator suites remain deferred. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Discover the Web assembly's built-in JS callback provider, spread its descriptors through generated contexts via UnsafeAccessor, and add callback-specific BrowserFile contracts only to the owning Web build. Behavior: changed: generated application contexts now include framework-owned Web callbacks State: builds; provider coverage follows in the next commit Review hint: the generator only knows the provider assembly and factory shape; callback signatures and JSON contracts remain owned by Components.Web. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Verify generated contexts reach the Components.Web callback provider through the emitted UnsafeAccessor and include the complete built-in callback descriptor set. Behavior: preserved State: complete for framework callback metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Delete the task-result generic reconstruction helper and broad linker warning XML now that pending calls retain result types and reflection suppressions sit beside the exact legacy operations. Behavior: preserved State: complete Review hint: both deleted artifacts have no remaining callers or live suppression targets. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Prove the owning Components.Web serializer context emits BrowserFile and BrowserFile[] metadata and that the built-in NotifyChange descriptor consumes the array contract. Behavior: preserved State: complete Review hint: this test guards the Web-only side of the existing COMPONENTS_WEBASSEMBLY conditional. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Migrate the generator tests from the PR-local xUnit v3 executable to the repository's current MSTest-on-MTP pattern, so the standard Test target owns discovery, filtering, and exit handling. Behavior: preserved State: complete Review hint: test scenarios and generator harness behavior are unchanged; only framework attributes, assertions, and project runner wiring move to MSTest. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
javiercn
force-pushed
the
javiercn-aot-stack-2-jsinterop
branch
from
August 11, 2026 10:15
f292100 to
e76b510
Compare
javiercn
marked this pull request as ready for review
August 11, 2026 14:07
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a source-generated-first dispatch path for JS-to-.NET [JSInvokable] calls by adding a JSInvokableMethodDescriptor contract and resolver chain, while keeping reflection as the default compatibility fallback. It also removes runtime reconstruction of outbound .NET -> JS result types by keeping the generic result type alive in pending-call objects.
Changes:
- Add
JSInvokableMethodDescriptor(+ resolver infrastructure) and refactorDotNetDispatcherto resolve + execute descriptors, falling back to reflection when enabled. - Replace runtime generic result recovery for
.NET -> JSpending calls withPendingAsyncCall<TValue>and anIPendingAsyncCalltable. - Add Components-side metadata context surface for JS descriptors, framework-provided built-in descriptors, and generator + tests for descriptor emission.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/JSInterop/Microsoft.JSInterop/test/Microsoft.JSInterop.Tests.csproj | Suppress experimental warning in tests. |
| src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs | Update argument parsing tests + add nullable result test. |
| src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherDescriptorTest.cs | New tests for descriptor resolution, ordering, fallback, and switches. |
| src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt | Record new public API surface for descriptors + JSRuntime.InvokableMethods. |
| src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.WarningSuppressions.xml | Remove linker suppression file tied to old reflection implementation. |
| src/JSInterop/Microsoft.JSInterop/src/Microsoft.JSInterop.csproj | Suppress experimental warning in product project. |
| src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs | Switch pending-call storage to typed pending calls + add invokable resolver hook. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs | Remove runtime generic task/TCS helper. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/SourceGeneratedJSInvokableMethodResolver.cs | New resolver for source-generated descriptor lookup + inheritance coverage logic. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/ReflectionJSInvokableMethodResolver.cs | New reflection-based compatibility resolver returning executable descriptors. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/PendingAsyncCall.cs | New PendingAsyncCall<TValue> implementing IPendingAsyncCall. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSInvokableMethodResolverFactory.cs | New resolver factory + reflection-enable feature switch. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSInvokableMethodInfo.cs | New method identity record used for resolution keys. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/JSInvokableMethodDescriptor.cs | New public experimental dispatch descriptor contract + method kind enum. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/IPendingAsyncCall.cs | New interface for completing pending .NET -> JS calls without reflection. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/IJSInvokableMethodResolver.cs | New internal resolver abstraction used by the composite chain. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs | Refactor dispatcher to resolve + invoke descriptors; unify sync/async completion. |
| src/JSInterop/Microsoft.JSInterop/src/Infrastructure/CompositeJSInvokableMethodResolver.cs | New composite resolver that tries generated then reflection. |
| src/Components/WebAssembly/WebAssembly/test/Services/WebAssemblyHostSerializationContextTest.cs | Update test contexts for new metadata context abstract member. |
| src/Components/Web/test/Rendering/WebRendererJsonResolverTest.cs | Add coverage for framework callback contracts including BrowserFile shapes. |
| src/Components/Web/test/Metadata/ComponentMetadataServiceCollectionExtensionsTest.cs | Update test contexts for new metadata context abstract member. |
| src/Components/Web/src/PublicAPI.Unshipped.txt | Record new RazorComponentsMetadataContext.JSInvokableMethods API. |
| src/Components/Web/src/Metadata/RazorComponentsMetadataContext.cs | Add abstract JSInvokableMethods to metadata contexts (experimental surface). |
| src/Components/Web/src/Metadata/BuiltInJSInvokableMethodDescriptors.cs | Add built-in framework callback descriptors (WebRenderer, Virtualize, InputFile). |
| src/Components/Web/src/Internal/WebJSInteropSerializerContext.cs | Add BrowserFile JSON contracts for non-WASM build. |
| src/Components/Server/test/ProtectedBrowserStorageSerializerOptionsTest.cs | Update test context to satisfy new abstract metadata member. |
| src/Components/Server/test/Microsoft.AspNetCore.Components.Server.Tests.csproj | Add RemoteExecutor reference for new tests. |
| src/Components/Server/test/Circuits/RemoteJSRuntimeMetadataTest.cs | New tests for context ordering, reflection fallback, and switch behavior. |
| src/Components/Server/src/Circuits/RemoteJSRuntime.cs | Snapshot metadata contexts into runtime InvokableMethods list for dispatch. |
| src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj | Suppress experimental warning + exclude generator tests from this project. |
| src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Generators.Tests/RazorComponentsMetadataGeneratorTestBase.cs | New Roslyn harness for generator tests + compilation/load helpers. |
| src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Generators.Tests/RazorComponentsMetadataGeneratorJSInteropTests.cs | New generator tests covering descriptor emission, overrides, ordering, built-ins. |
| src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Generators.Tests/Microsoft.AspNetCore.Components.Endpoints.Generators.Tests.csproj | New MSTest/MTP-based generator test project. |
| src/Components/Endpoints/test/DependencyInjection/ComponentJsonMetadataIsolationTest.cs | Update test contexts for new metadata context abstract member. |
| src/Components/Endpoints/gen/WellKnownTypes.cs | Add well-known symbols for generator analysis (JSInvokable, Task/ValueTask, etc.). |
| src/Components/Endpoints/gen/TypeAccessibility.cs | Add type/nameability checks for generator emission boundaries. |
| src/Components/Endpoints/gen/SymbolHelpers.cs | Add symbol helpers (qualified names, type enumeration, partial checks, etc.). |
| src/Components/Endpoints/gen/RazorComponentsMetadataGenerator.JSInterop.cs | Add generator collection logic for JS invokables and inheritance metadata. |
| src/Components/Endpoints/gen/RazorComponentsMetadataGenerator.cs | Add incremental generator entry + model building and context resolution. |
| src/Components/Endpoints/gen/Models/MetadataContextModel.cs | Add generator models + comparers for incremental caching. |
| src/Components/Endpoints/gen/Microsoft.AspNetCore.Components.Endpoints.Generators.csproj | New analyzer project for the metadata generator. |
| src/Components/Endpoints/gen/Emitters/RazorComponentsMetadataGenerator.Emitter.cs | Emit metadata context partial implementation + descriptors + invocation bodies. |
| src/Components/Endpoints/gen/DiagnosticDescriptors.cs | Add generator diagnostic for non-partial context declarations. |
| src/Components/Endpoints/gen/CodeWriter.cs | Add minimal writer used for readable generated output. |
| src/Components/ComponentsNoDeps.slnf | Include generator project + tests in solution filter. |
| src/Components/Components.slnf | Include generator project + tests in solution filter. |
| AspNetCore.slnx | Include generator project + tests in main solution map. |
Suppressed comments (1)
src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs:338
- When a JS->.NET completion arrives for an unknown taskId (e.g., the .NET side timed out/canceled and removed it), any received byte-array payloads remain in ByteArraysToBeRevived until another transfer starts. This can retain large buffers unnecessarily and can also interfere with subsequent calls if no new byte-array transfer begins. Clearing ByteArraysToBeRevived before returning avoids leaking/bleeding data from late completions.
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+99
to
+102
| if (invoked && !invocationResult.IsCompletedSuccessfully) | ||
| { | ||
| _ = invocationResult.AsTask(); | ||
| } |
Comment on lines
+71
to
+74
| if (actualCount > parameterTypes.Length) | ||
| { | ||
| throw new JsonException($"Unexpected JSON token {GetTokenName(argumentsElement[parameterTypes.Length])}. Ensure that the call to `{methodIdentifier}' is supplied with exactly '{parameterTypes.Length}' parameters."); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This is stack layer 2 of 6 for #68332, depends on #68295, and targets
javiercn-aot-stack-1-stj; the review range is the stacked PR only (9ca33ec5b0..e76b510be8, 47 files / 12 commits). It makes JS-to-.NET dispatch generated-first and removes runtime reconstruction of outbound result types while preserving reflection as the default compatibility resolver. Component/bindable metadata, framework component providers, strict switches, and E2E proof remain in later stack layers.Design
The public extension point is an executable descriptor rather than a public resolver abstraction. It contains both protocol keys and the entire typed invocation operation, so generated code—not
DotNetDispatcher—owns argument deserialization, method invocation, awaiting, and result serialization.Two existing experimental owners expose flat descriptor lists; all resolver composition remains internal:
The key design decisions are:
JSInvokableMethodDescriptor;DotNetDispatchernever branches on implementation type.Microsoft.JSInterop.JSInvokableMethodResolution.IsReflectionEnabledByDefault, defaults totrue, and is the only resolver requiring dynamic-code/trimming annotations.JsonSerializerOptionssupplies contracts.Microsoft.JSInteroptherefore gains no Components dependency.OverrideBlockerand type-coverage descriptors prevent generated lookup from incorrectly inheriting a base method through an undescribed override/new slot; an uncovered case misses and reaches reflection only when compatibility is enabled.BrowserFilecontracts; the application generator reaches that internal provider through an emittedUnsafeAccessorinstead of copying framework details.Implementation
Outbound .NET-to-JS calls now retain
TValuein the pending-call object. When the wire result returns, virtual dispatch lands in a generic body whereTValueis still statically known; the deletedTaskGenericsUtilno longer needs to recover it fromTypeand construct generic helpers at run time.For incoming JS-to-.NET calls,
DotNetDispatchernow performs protocol work only: resolve the receiver, keep__Disposeoutside method metadata, resolve one descriptor, invoke it, and normalize synchronous/asynchronous completion toValueTask<string?>.The resolver factory establishes the compatibility boundary. A runtime with descriptors gets generated lookup first; reflection is appended only when its feature guard is enabled. The reflection resolver contains all scans,
MethodInfo.Invoke, argument parsing, return adapters, caches, and their exact suppressions, so disabling it gives the linker one removable dynamic-code root.The application generator walks application/reference symbols, emits one descriptor per usable
[JSInvokable]alias, adds blockers/coverage for inheritance, and emits typed delegates. The representative generated shape below is de-templatized fromRazorComponentsMetadataGenerator.Emitter.cs; the concrete generic calls are what make the path statically analyzable.RemoteJSRuntimesnapshots registered contexts in registration order and exposes the flattened list through the runtime hook. Generated duplicate contributions with the sameMethodKeykeep the first registration; conflicting lookup keys still fail deterministically.Framework-owned callbacks are the same descriptor equivalence class with one ownership delta: Components.Web supplies seven typed instance descriptors (four
WebRendereroperations, two virtualization spacers, andInputFile.NotifyChange).BrowserFilemetadata is emitted only by the owning Web build because the shared serializer source also compiles into WebAssembly, where the internal type is inaccessible.The generator test project follows the current Components testing stack: MSTest v4 on Microsoft.Testing.Platform, the repository
Testtarget, and the standard non-quarantined category filter. The former custom xUnit v3RunTeststarget and legacy-argument workaround are intentionally absent.Coverage is organized by behavioral equivalence class rather than by file:
(assembly, alias)(type, alias), base-type walk, constructed generics, duplicate aliases/contributionsTask,Task<T>, derived task,ValueTask,ValueTask<T>, failuresDotNetObjectReference<T>shape__Disposebypass, pending byte-array cleanup, cancellation/failure of outbound callsJsonElementBrowserFileandBrowserFile[]in Web; inaccessible shared WebAssembly build excludedTesttargetOutcome
Microsoft.JSInterop.Testsruntime matrixTesttargetRemoteJSRuntimeMetadataTestcontext ordering/fallbackWebRendererJsonResolverTestresolver order + BrowserFile contractsWasmLinkerTestandLinkabilityCheckersucceeded with 0 warnings/errors before this testing-only restack; all 11 product/test patches retain identical stable patch IDsDefault/JIT compatibility remains unchanged because method reflection is enabled unless explicitly disabled. Strict reflection-off Native AOT and end-to-end feature proof are intentionally deferred to the final stack layer.