Skip to content

Commit ef83e33

Browse files
authored
Change JSInterop to avoid using async locals (dotnet/extensions#2163)
* Remove the use of async local JSRuntime * Update DotNetDispatcher to accept a JSRuntime instance rather than use a ambient value. * Modify DotNetObjectReference to start tracking it's value during serialization. \n\nCommit migrated from dotnet/extensions@ae9878b
1 parent c717230 commit ef83e33

20 files changed

+431
-362
lines changed

src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ module DotNet {
5555
return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args);
5656
}
5757

58-
function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T {
58+
function invokePossibleInstanceMethod<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): T {
5959
const dispatcher = getRequiredDispatcher();
6060
if (dispatcher.invokeDotNetFromJS) {
6161
const argsJson = JSON.stringify(args, argReplacer);
@@ -66,7 +66,7 @@ module DotNet {
6666
}
6767
}
6868

69-
function invokePossibleInstanceMethodAsync<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, ...args: any[]): Promise<T> {
69+
function invokePossibleInstanceMethodAsync<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): Promise<T> {
7070
if (assemblyName && dotNetObjectId) {
7171
throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ;
7272
}
@@ -273,7 +273,7 @@ module DotNet {
273273
}
274274

275275
public dispose() {
276-
const promise = invokePossibleInstanceMethodAsync<any>(null, '__Dispose', this._id);
276+
const promise = invokePossibleInstanceMethodAsync<any>(null, '__Dispose', this._id, null);
277277
promise.catch(error => console.error(error));
278278
}
279279

src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ public abstract partial class JSRuntime : Microsoft.JSInterop.IJSRuntime
4848
{
4949
protected JSRuntime() { }
5050
protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
51+
protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
5152
protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson);
5253
protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId);
5354
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) { throw null; }
5455
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; }
55-
public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { }
5656
}
5757
public static partial class JSRuntimeExtensions
5858
{
@@ -72,8 +72,8 @@ namespace Microsoft.JSInterop.Infrastructure
7272
{
7373
public static partial class DotNetDispatcher
7474
{
75-
public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
76-
public static void EndInvokeJS(string arguments) { }
77-
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
75+
public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
76+
public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { }
77+
public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
7878
}
7979
}

src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ public abstract partial class JSRuntime : Microsoft.JSInterop.IJSRuntime
4848
{
4949
protected JSRuntime() { }
5050
protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
51+
protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
5152
protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson);
5253
protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId);
5354
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) { throw null; }
5455
public System.Threading.Tasks.ValueTask<TValue> InvokeAsync<TValue>(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; }
55-
public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { }
5656
}
5757
public static partial class JSRuntimeExtensions
5858
{
@@ -72,8 +72,8 @@ namespace Microsoft.JSInterop.Infrastructure
7272
{
7373
public static partial class DotNetDispatcher
7474
{
75-
public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
76-
public static void EndInvokeJS(string arguments) { }
77-
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
75+
public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
76+
public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { }
77+
public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
7878
}
7979
}

src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4-
using Microsoft.JSInterop.Infrastructure;
5-
64
namespace Microsoft.JSInterop
75
{
86
/// <summary>
@@ -17,7 +15,7 @@ public static class DotNetObjectReference
1715
/// <returns>An instance of <see cref="DotNetObjectReference{TValue}" />.</returns>
1816
public static DotNetObjectReference<TValue> Create<TValue>(TValue value) where TValue : class
1917
{
20-
return new DotNetObjectReference<TValue>(DotNetObjectReferenceManager.Current, value);
18+
return new DotNetObjectReference<TValue>(value);
2119
}
2220
}
2321
}

src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Text.Json.Serialization;
5+
using System.Diagnostics;
66
using Microsoft.JSInterop.Infrastructure;
77

88
namespace Microsoft.JSInterop
@@ -14,22 +14,18 @@ namespace Microsoft.JSInterop
1414
/// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code.
1515
/// </summary>
1616
/// <typeparam name="TValue">The type of the value to wrap.</typeparam>
17-
[JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))]
1817
public sealed class DotNetObjectReference<TValue> : IDotNetObjectReference, IDisposable where TValue : class
1918
{
20-
private readonly DotNetObjectReferenceManager _referenceManager;
2119
private readonly TValue _value;
22-
private readonly long _objectId;
20+
private long _objectId;
21+
private JSRuntime _jsRuntime;
2322

2423
/// <summary>
2524
/// Initializes a new instance of <see cref="DotNetObjectReference{TValue}" />.
2625
/// </summary>
27-
/// <param name="referenceManager"></param>
2826
/// <param name="value">The value to pass by reference.</param>
29-
internal DotNetObjectReference(DotNetObjectReferenceManager referenceManager, TValue value)
27+
internal DotNetObjectReference(TValue value)
3028
{
31-
_referenceManager = referenceManager;
32-
_objectId = _referenceManager.TrackObject(this);
3329
_value = value;
3430
}
3531

@@ -50,8 +46,30 @@ internal long ObjectId
5046
get
5147
{
5248
ThrowIfDisposed();
49+
Debug.Assert(_objectId != 0, "Accessing ObjectId without tracking is always incorrect.");
50+
5351
return _objectId;
5452
}
53+
set
54+
{
55+
ThrowIfDisposed();
56+
_objectId = value;
57+
}
58+
}
59+
60+
internal JSRuntime JSRuntime
61+
{
62+
get
63+
{
64+
ThrowIfDisposed();
65+
return _jsRuntime;
66+
}
67+
set
68+
{
69+
ThrowIfDisposed();
70+
_jsRuntime = value;
71+
}
72+
5573
}
5674

5775
object IDotNetObjectReference.Value => Value;
@@ -68,11 +86,15 @@ public void Dispose()
6886
if (!Disposed)
6987
{
7088
Disposed = true;
71-
_referenceManager.ReleaseDotNetObject(_objectId);
89+
90+
if (_jsRuntime != null)
91+
{
92+
_jsRuntime.ReleaseObjectReference(_objectId);
93+
}
7294
}
7395
}
7496

75-
private void ThrowIfDisposed()
97+
internal void ThrowIfDisposed()
7698
{
7799
if (Disposed)
78100
{

src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ public static class DotNetDispatcher
2727
/// <summary>
2828
/// Receives a call from JS to .NET, locating and invoking the specified method.
2929
/// </summary>
30+
/// <param name="jsRuntime">The <see cref="JSRuntime"/>.</param>
3031
/// <param name="assemblyName">The assembly containing the method to be invoked.</param>
3132
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
3233
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
3334
/// <param name="argsJson">A JSON representation of the parameters.</param>
3435
/// <returns>A JSON representation of the return value, or null.</returns>
35-
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
36+
public static string Invoke(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
3637
{
3738
// This method doesn't need [JSInvokable] because the platform is responsible for having
3839
// some way to dispatch calls here. The logic inside here is the thing that checks whether
@@ -42,41 +43,38 @@ public static string Invoke(string assemblyName, string methodIdentifier, long d
4243
IDotNetObjectReference targetInstance = default;
4344
if (dotNetObjectId != default)
4445
{
45-
targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId);
46+
targetInstance = jsRuntime.GetObjectReference(dotNetObjectId);
4647
}
4748

48-
var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
49+
var syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson);
4950
if (syncResult == null)
5051
{
5152
return null;
5253
}
5354

54-
return JsonSerializer.Serialize(syncResult, JsonSerializerOptionsProvider.Options);
55+
return JsonSerializer.Serialize(syncResult, jsRuntime.JsonSerializerOptions);
5556
}
5657

5758
/// <summary>
5859
/// Receives a call from JS to .NET, locating and invoking the specified method asynchronously.
5960
/// </summary>
61+
/// <param name="jsRuntime">The <see cref="JSRuntime"/>.</param>
6062
/// <param name="callId">A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required.</param>
6163
/// <param name="assemblyName">The assembly containing the method to be invoked.</param>
6264
/// <param name="methodIdentifier">The identifier of the method to be invoked. The method must be annotated with a <see cref="JSInvokableAttribute"/> matching this identifier string.</param>
6365
/// <param name="dotNetObjectId">For instance method calls, identifies the target object.</param>
6466
/// <param name="argsJson">A JSON representation of the parameters.</param>
6567
/// <returns>A JSON representation of the return value, or null.</returns>
66-
public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
68+
public static void BeginInvokeDotNet(JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson)
6769
{
6870
// This method doesn't need [JSInvokable] because the platform is responsible for having
6971
// some way to dispatch calls here. The logic inside here is the thing that checks whether
7072
// the targeted method has [JSInvokable]. It is not itself subject to that restriction,
7173
// because there would be nobody to police that. This method *is* the police.
7274

73-
// DotNetDispatcher only works with JSRuntimeBase instances.
74-
// If the developer wants to use a totally custom IJSRuntime, then their JS-side
75-
// code has to implement its own way of returning async results.
76-
var jsRuntimeBaseInstance = (JSRuntime)JSRuntime.Current;
77-
7875
// Using ExceptionDispatchInfo here throughout because we want to always preserve
7976
// original stack traces.
77+
8078
object syncResult = null;
8179
ExceptionDispatchInfo syncException = null;
8280
IDotNetObjectReference targetInstance = null;
@@ -85,10 +83,10 @@ public static void BeginInvokeDotNet(string callId, string assemblyName, string
8583
{
8684
if (dotNetObjectId != default)
8785
{
88-
targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId);
86+
targetInstance = jsRuntime.GetObjectReference(dotNetObjectId);
8987
}
9088

91-
syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
89+
syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson);
9290
}
9391
catch (Exception ex)
9492
{
@@ -103,7 +101,7 @@ public static void BeginInvokeDotNet(string callId, string assemblyName, string
103101
else if (syncException != null)
104102
{
105103
// Threw synchronously, let's respond.
106-
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId);
104+
jsRuntime.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId);
107105
}
108106
else if (syncResult is Task task)
109107
{
@@ -115,20 +113,20 @@ public static void BeginInvokeDotNet(string callId, string assemblyName, string
115113
{
116114
var exception = t.Exception.GetBaseException();
117115

118-
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId);
116+
jsRuntime.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId);
119117
}
120118

121119
var result = TaskGenericsUtil.GetTaskResult(task);
122-
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId);
120+
jsRuntime.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId);
123121
}, TaskScheduler.Current);
124122
}
125123
else
126124
{
127-
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId);
125+
jsRuntime.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId);
128126
}
129127
}
130128

131-
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson)
129+
private static object InvokeSynchronously(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson)
132130
{
133131
AssemblyKey assemblyKey;
134132
if (objectReference is null)
@@ -154,7 +152,7 @@ private static object InvokeSynchronously(string assemblyName, string methodIden
154152

155153
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
156154

157-
var suppliedArgs = ParseArguments(methodIdentifier, argsJson, parameterTypes);
155+
var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes);
158156

159157
try
160158
{
@@ -173,7 +171,7 @@ private static object InvokeSynchronously(string assemblyName, string methodIden
173171
}
174172
}
175173

176-
internal static object[] ParseArguments(string methodIdentifier, string arguments, Type[] parameterTypes)
174+
internal static object[] ParseArguments(JSRuntime jsRuntime, string methodIdentifier, string arguments, Type[] parameterTypes)
177175
{
178176
if (parameterTypes.Length == 0)
179177
{
@@ -198,7 +196,7 @@ internal static object[] ParseArguments(string methodIdentifier, string argument
198196
throw new InvalidOperationException($"In call to '{methodIdentifier}', parameter of type '{parameterType.Name}' at index {(index + 1)} must be declared as type 'DotNetObjectRef<{parameterType.Name}>' to receive the incoming value.");
199197
}
200198

201-
suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, JsonSerializerOptionsProvider.Options);
199+
suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, jsRuntime.JsonSerializerOptions);
202200
index++;
203201
}
204202

@@ -247,18 +245,13 @@ static bool IsIncorrectDotNetObjectRefUse(Type parameterType, Utf8JsonReader jso
247245
/// method is responsible for handling any possible exception generated from the arguments
248246
/// passed in as parameters.
249247
/// </remarks>
248+
/// <param name="jsRuntime">The <see cref="JSRuntime"/>.</param>
250249
/// <param name="arguments">The serialized arguments for the callback completion.</param>
251250
/// <exception cref="Exception">
252251
/// This method can throw any exception either from the argument received or as a result
253252
/// of executing any callback synchronously upon completion.
254253
/// </exception>
255-
public static void EndInvokeJS(string arguments)
256-
{
257-
var jsRuntimeBase = (JSRuntime)JSRuntime.Current;
258-
ParseEndInvokeArguments(jsRuntimeBase, arguments);
259-
}
260-
261-
internal static void ParseEndInvokeArguments(JSRuntime jsRuntimeBase, string arguments)
254+
public static void EndInvokeJS(JSRuntime jsRuntime, string arguments)
262255
{
263256
var utf8JsonBytes = Encoding.UTF8.GetBytes(arguments);
264257

@@ -281,7 +274,7 @@ internal static void ParseEndInvokeArguments(JSRuntime jsRuntimeBase, string arg
281274
var success = reader.GetBoolean();
282275

283276
reader.Read();
284-
jsRuntimeBase.EndInvokeJS(taskId, success, ref reader);
277+
jsRuntime.EndInvokeJS(taskId, success, ref reader);
285278

286279
if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray)
287280
{

0 commit comments

Comments
 (0)