[API Proposal] Automatically marshal AbortSignal and CancellationToken arguments
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
- Issue type
- Feature
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- csharp, javascript, typescript
Research direction
Read the revised #70326 specification, the draft implementation in #70328, and the named files DotNetCancellationTokenReference.cs, JSCancellationTokenReference.cs, and Microsoft.JSInterop.ts. Review the API proposal and the existing validation results, then identify the unresolved API design and runtime coverage questions. Done means the API has completed review and the remaining runtime scenarios have been validated; the issue explicitly says it is experimental and not API-approved.
Written by the indexing model from the issue text.
Description
Background and Motivation
Revised again to the user-confirmed API: pass raw AbortSignal/CancellationToken values to ordinary interop. This supersedes both the original call-level cancellation and the later manual-reference-factory proposal.
Specification/examples: #70326. Implementation: draft #70328, commit dcd2191693. The user explicitly requested dotnetObject.invokeMethodAsync("Yolo", abortSignal), the equivalent C# argument behavior, and static methods without an application .NET object reference. The three concrete examples below were shown together and the user selected “Yes—implement this API”. That confirmation is not API-review approval.
The runtimes own reference setup, sharing, rollback and cleanup. Application code does not call a cancellation-reference factory, construct a wrapper or dispose a bridge. Cancellation remains a parameter value; a receiver can ignore it, catch it and return normally, or retain it after returning.
Status: experimental, not API-approved or api-ready-for-review. Proposed champion remains @javiercn as issue owner; meeting participation is unconfirmed.
Proposed API
No new application invocation method, factory, reference type or JS wrapper is required. Existing signatures remain:
DotNetObject.invokeMethodAsync<T>(methodIdentifier: string, ...args: any[]): Promise<T>;
DotNet.invokeMethodAsync<T>(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise<T>;
ValueTask<TValue> IJSRuntime.InvokeAsync<TValue>(string identifier, object?[]? args);
ValueTask<TValue> IJSRuntime.InvokeAsync<TValue>(
string identifier, CancellationToken cancellationToken, object?[]? args);
New public marshalling convention: a raw JavaScript AbortSignal argument maps automatically to a normal .NET CancellationToken parameter, in any position or inside supported argument objects. A raw CancellationToken argument maps automatically to a native JavaScript AbortSignal. It is not the separate invocation-control token. Pre-aborted input still invokes .NET; there is no special promise cancellation outcome.
The earlier createCancellationTokenReference, DotNetCancellationTokenReference.CreateAsync, explicit owners and invokeMethodAsyncWithCancellation APIs are removed from the draft. There are no new application-facing JavaScript exports.
The current prototype retains this public framework callback plumbing because existing static JSInvokable discovery uses exported types. It is not a consumer factory and needs API review/placement discussion. This is the complete new .NET surface against main, recorded in PublicAPI.Unshipped.txt:
namespace Microsoft.JSInterop
{
+ public sealed class JSCancellationTokenReference : System.IAsyncDisposable
+ {
+ public System.Threading.CancellationToken Token { get; }
+ [JSInvokable("CreateCancellationTokenReference")]
+ public static DotNetObjectReference<JSCancellationTokenReference> Create(
+ IJSObjectReference signal, bool isAborted);
+ [JSInvokable]
+ public System.Threading.Tasks.Task CancelAsync();
+ [JSInvokable]
+ public System.Threading.Tasks.ValueTask DisposeAsync();
+ }
}
No constructor is public. Framework-only callback naming/placement is provisional; applications write none of these calls.
Usage Examples
JS to .NET instance method
const controller = new AbortController();
const pending = dotnetObject.invokeMethodAsync("Yolo", controller.signal);
controller.abort();
const result = await pending;
[JSInvokable]
public Task<string> Yolo(CancellationToken cancellationToken);
JS to .NET static method
const controller = new AbortController();
const pending = DotNet.invokeMethodAsync("MyApp", "YoloStatic", controller.signal);
controller.abort();
const result = await pending;
[JSInvokable]
public static Task<string> YoloStatic(CancellationToken cancellationToken);
No application DotNetObjectReference is needed for the static call. Static runtime selection follows the existing API.
.NET to JavaScript
using var cts = new CancellationTokenSource();
var pending = jsRuntime.InvokeAsync<string>("yolo", args: [cts.Token]);
cts.Cancel();
var result = await pending;
globalThis.yolo = async function (signal) {
const response = await fetch("/data", { signal });
return await response.text();
};
args: [cts.Token] is intentional: it supplies an ordinary argument. Passing a CancellationToken as the overload's control parameter still cancels only the managed wait. Both token/signal values can remain live after a call returns, with no user-created or user-disposed bridge.
Alternative Designs
- Call-ID cancellation, final-token injection and special invocation methods: the wrong abstraction; removed.
- Explicit async reference factories/owners: rejected by the user in favor of automatic raw arguments; removed.
- Hidden asynchronous work inside a synchronous converter: instead the JS async invocation pipeline prepares signal references, and the .NET token converter emits a descriptor consumed by the JS reviver.
- Cleanup on call completion: would break retained values. Notification, browser GC and runtime teardown own cleanup instead.
- Retain every successful call's bridge until runtime end: avoid this with weak signal ownership and real FinalizationRegistry cleanup.
- Global-only signal setup: instance calls use their existing dispatcher; static calls use the existing static dispatcher. No extra application object is needed.
- Re-serialize application objects after an await: would rerun getters/toJSON and break byte-array staging. The implementation snapshots once, records signal locations, then edits the data-only snapshot after setup.
Risks
- This is a new public marshalling behavior despite unchanged application method signatures. It and the framework callback surface require API review. PublicAPI.Unshipped does not confer approval.
- WeakRef/FinalizationRegistry are browser prerequisites. GC cleanup is eventual/nondeterministic. Live uncanceled values retain bridge resources until notification, collection or runtime teardown; no deterministic reclamation deadline is promised.
- An outgoing observation is an ordinary pending interop promise until cancellation/collection/teardown. Impact on circuit pause/resume and other lifecycle policies requires separate validation.
- Incoming repeated/concurrent uses share a bridge by signal identity in a runtime. Outgoing token occurrences can have distinct native signal identities; they observe the same original token. This avoids shared-ID revocation races with in-flight payloads.
- Async preparation and snapshot copying introduce allocations/round trips; large mixed payloads require an extra data-only JSON traversal. No benchmark is claimed.
- Setup failures use normal invocation failure handling. Notification/cleanup failures are logged explicitly rather than imposing a new invocation outcome.
- Generic createJSObjectReference(signal) arguments retain their generic reference meaning. Only raw signal arguments get this automatic projection.
- Server/WebAssembly and Chromium GC are validated separately. WebView, Native AOT/trimming publish, multithreaded WebAssembly, actual mixed-runtime E2E, Auto transitions, reconnect/resume and non-Chromium remain unverified. Managed runtime cleanup is implemented; comprehensive remote cleanup after disconnection remains an unverified boundary.
Source justifications
Raw API: "dotnetObject.invokeMethodAsync(\"Yolo\", abortSignal) should be what the user writes. Equivalent on the c# side" — user clarification captured in revised #70326
Static calls: "And obviously should also work with \"static methods\" without requiring a dotnet reference" — user clarification captured in revised #70326
Automatic values: "Application code does not create, wrap, or dispose interop cancellation references." — implementation README
No forced promise result: "Abort signals cancel the token, not the interop promise" — implementation README
Getter/byte preservation: "Application getters and toJSON execute once" — revised specification and real-browser snapshot test
Actual callback surface: declarations above match PublicAPI.Unshipped.txt and JSCancellationTokenReference.cs in [dcd2191693](https://github.com/dotnet/aspnetcore/commit/dcd2191693211df7eef0d3a9beeae15eab9f704e)
Cleanup: "Collection is nondeterministic and does not request cancellation." — implementation README and both-runtime GC tests using Chrome HeapProfiler.collectGarbage
Control-token distinction: "The separate invocation-control token still cancels only the managed wait" — implementation README and both-runtime control-token test
Full validation commands, red/green evidence and runtime boundaries are on #70326 and draft #70328. This revision replaces the prior proposal rather than asserting approval of it.
- Dominant language
- C#
- Stars
- 38.5k
- Forks
- 11.6k
- Avg merge
- 3d 1h
- Merged PRs (30d)
- 289
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from dotnet/aspnetcore
-
area-networking
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70545 · 1 comment ·
Maintainers usually reply within 1 day
-
area-infrastructure
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
dotnet/aspnetcore#70524 · 1 comment ·
Maintainers usually reply within 1 day
-
[OpenTelemetry] Hosting replaces an Activity.DisplayName that the app set, when a route matchesOpenarea-hosting Regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70516 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
[SSE] ServerSentEventsResult.WrapEvents is missing [EnumeratorCancellation], so RequestAborted never reaches the app's generator on the IAsyncEnumerable<T> overloadPossibly taken @bharatprajapati-coder claimed this 1 day ago. Openarea-minimal
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
dotnet/aspnetcore#70419 · 2 comments ·
Maintainers usually reply within 1 day
-
[Blazor] DefaultInMemoryCircuitPersistenceProvider never disposes the retention CancellationTokenSource when a persist is rejected at capacityPossibly taken @Vinoth2562000 claimed this 1 day ago. Openarea-blazor
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
dotnet/aspnetcore#70418 · 1 comment · 1 assignee ·
Maintainers usually reply within 1 day
All issues in dotnet/aspnetcore
Similar issues
-
bug emitter:client:csharp
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
microsoft/typespec#12572 · 1 comment · 1 reaction ·
Maintainers usually reply within 1 day
-
needs-area-label
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 2 days
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledOpenbug
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
newrelic/newrelic-dotnet-agent#3949 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 87/100
Maintainers usually reply within 1 day
-
priority:P2
Difficulty 2/5 1-3 hours Newbie friendliness 80/100