Skip to content

Commit d6bfc28

Browse files
authored
Special case Disposing DotNetObjectReferences (#2176)
* Special case Disposing DotNetObjectReferences This removes a public JSInvokable method required for disposing DotNetObjectReferences
1 parent 3a5b3a8 commit d6bfc28

13 files changed

+109
-62
lines changed

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ 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[]): Promise<T> {
70+
if (assemblyName && dotNetObjectId) {
71+
throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ;
72+
}
73+
7074
const asyncCallId = nextAsyncCallId++;
7175
const resultPromise = new Promise<T>((resolve, reject) => {
7276
pendingAsyncCalls[asyncCallId] = { resolve, reject };
@@ -269,10 +273,7 @@ module DotNet {
269273
}
270274

271275
public dispose() {
272-
const promise = invokeMethodAsync<any>(
273-
'Microsoft.JSInterop',
274-
'DotNetDispatcher.ReleaseDotNetObject',
275-
this._id);
276+
const promise = invokePossibleInstanceMethodAsync<any>(null, '__Dispose', this._id);
276277
promise.catch(error => console.error(error));
277278
}
278279

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ public static partial class DotNetDispatcher
88
public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
99
public static void EndInvoke(string arguments) { }
1010
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
11-
[Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")]
12-
public static void ReleaseDotNetObject(long dotNetObjectId) { }
1311
}
1412
public static partial class DotNetObjectRef
1513
{
@@ -18,7 +16,7 @@ public static partial class DotNetObjectRef
1816
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
1917
{
2018
internal DotNetObjectRef() { }
21-
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
19+
public TValue Value { get { throw null; } }
2220
public void Dispose() { }
2321
}
2422
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ public static partial class DotNetDispatcher
88
public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
99
public static void EndInvoke(string arguments) { }
1010
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
11-
[Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")]
12-
public static void ReleaseDotNetObject(long dotNetObjectId) { }
1311
}
1412
public static partial class DotNetObjectRef
1513
{
@@ -18,7 +16,7 @@ public static partial class DotNetObjectRef
1816
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
1917
{
2018
internal DotNetObjectRef() { }
21-
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
19+
public TValue Value { get { throw null; } }
2220
public void Dispose() { }
2321
}
2422
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime

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

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace Microsoft.JSInterop
1818
/// </summary>
1919
public static class DotNetDispatcher
2020
{
21+
private const string DisposeDotNetObjectReferenceMethodName = "__Dispose";
2122
internal static readonly JsonEncodedText DotNetObjectRefKey = JsonEncodedText.Encode("__dotNetObject");
2223

2324
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
@@ -38,7 +39,7 @@ public static string Invoke(string assemblyName, string methodIdentifier, long d
3839
// the targeted method has [JSInvokable]. It is not itself subject to that restriction,
3940
// because there would be nobody to police that. This method *is* the police.
4041

41-
var targetInstance = (object)null;
42+
IDotNetObjectRef targetInstance = default;
4243
if (dotNetObjectId != default)
4344
{
4445
targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
@@ -78,7 +79,7 @@ public static void BeginInvoke(string callId, string assemblyName, string method
7879
// original stack traces.
7980
object syncResult = null;
8081
ExceptionDispatchInfo syncException = null;
81-
object targetInstance = null;
82+
IDotNetObjectRef targetInstance = null;
8283

8384
try
8485
{
@@ -127,21 +128,28 @@ public static void BeginInvoke(string callId, string assemblyName, string method
127128
}
128129
}
129130

130-
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson)
131+
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectRef objectReference, string argsJson)
131132
{
132133
AssemblyKey assemblyKey;
133-
if (targetInstance != null)
134+
if (objectReference is null)
135+
{
136+
assemblyKey = new AssemblyKey(assemblyName);
137+
}
138+
else
134139
{
135140
if (assemblyName != null)
136141
{
137142
throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'.");
138143
}
139144

140-
assemblyKey = new AssemblyKey(targetInstance.GetType().Assembly);
141-
}
142-
else
143-
{
144-
assemblyKey = new AssemblyKey(assemblyName);
145+
if (string.Equals(DisposeDotNetObjectReferenceMethodName, methodIdentifier, StringComparison.Ordinal))
146+
{
147+
// The client executed dotNetObjectReference.dispose(). Dispose the reference and exit.
148+
objectReference.Dispose();
149+
return default;
150+
}
151+
152+
assemblyKey = new AssemblyKey(objectReference.Value.GetType().Assembly);
145153
}
146154

147155
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
@@ -150,7 +158,8 @@ private static object InvokeSynchronously(string assemblyName, string methodIden
150158

151159
try
152160
{
153-
return methodInfo.Invoke(targetInstance, suppliedArgs);
161+
// objectReference will be null if this call invokes a static JSInvokable method.
162+
return methodInfo.Invoke(objectReference?.Value, suppliedArgs);
154163
}
155164
catch (TargetInvocationException tie) // Avoid using exception filters for AOT runtime support
156165
{
@@ -280,22 +289,6 @@ internal static void ParseEndInvokeArguments(JSRuntimeBase jsRuntimeBase, string
280289
}
281290
}
282291

283-
/// <summary>
284-
/// Releases the reference to the specified .NET object. This allows the .NET runtime
285-
/// to garbage collect that object if there are no other references to it.
286-
///
287-
/// To avoid leaking memory, the JavaScript side code must call this for every .NET
288-
/// object it obtains a reference to. The exception is if that object is used for
289-
/// the entire lifetime of a given user's session, in which case it is released
290-
/// automatically when the JavaScript runtime is disposed.
291-
/// </summary>
292-
/// <param name="dotNetObjectId">The identifier previously passed to JavaScript code.</param>
293-
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))]
294-
public static void ReleaseDotNetObject(long dotNetObjectId)
295-
{
296-
DotNetObjectRefManager.Current.ReleaseDotNetObject(dotNetObjectId);
297-
}
298-
299292
private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, string methodIdentifier)
300293
{
301294
if (string.IsNullOrWhiteSpace(assemblyKey.AssemblyName))

src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ public static class DotNetObjectRef
1515
/// <returns>An instance of <see cref="DotNetObjectRef{TValue}" />.</returns>
1616
public static DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class
1717
{
18-
var objectId = DotNetObjectRefManager.Current.TrackObject(value);
19-
return new DotNetObjectRef<TValue>(objectId, value);
18+
return new DotNetObjectRef<TValue>(DotNetObjectRefManager.Current, value);
2019
}
2120
}
2221
}

src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.JSInterop
1010
internal class DotNetObjectRefManager
1111
{
1212
private long _nextId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1
13-
private readonly ConcurrentDictionary<long, object> _trackedRefsById = new ConcurrentDictionary<long, object>();
13+
private readonly ConcurrentDictionary<long, IDotNetObjectRef> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectRef>();
1414

1515
public static DotNetObjectRefManager Current
1616
{
@@ -25,15 +25,15 @@ public static DotNetObjectRefManager Current
2525
}
2626
}
2727

28-
public long TrackObject(object dotNetObjectRef)
28+
public long TrackObject(IDotNetObjectRef dotNetObjectRef)
2929
{
3030
var dotNetObjectId = Interlocked.Increment(ref _nextId);
3131
_trackedRefsById[dotNetObjectId] = dotNetObjectRef;
3232

3333
return dotNetObjectId;
3434
}
3535

36-
public object FindDotNetObject(long dotNetObjectId)
36+
public IDotNetObjectRef FindDotNetObject(long dotNetObjectId)
3737
{
3838
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
3939
? dotNetObjectRef

src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,53 @@ namespace Microsoft.JSInterop
1616
[JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))]
1717
public sealed class DotNetObjectRef<TValue> : IDotNetObjectRef, IDisposable where TValue : class
1818
{
19+
private readonly DotNetObjectRefManager _referenceManager;
20+
private readonly TValue _value;
21+
private readonly long _objectId;
22+
1923
/// <summary>
2024
/// Initializes a new instance of <see cref="DotNetObjectRef{TValue}" />.
2125
/// </summary>
22-
/// <param name="objectId">The object Id.</param>
26+
/// <param name="referenceManager"></param>
2327
/// <param name="value">The value to pass by reference.</param>
24-
internal DotNetObjectRef(long objectId, TValue value)
28+
internal DotNetObjectRef(DotNetObjectRefManager referenceManager, TValue value)
29+
{
30+
_referenceManager = referenceManager;
31+
_objectId = _referenceManager.TrackObject(this);
32+
_value = value;
33+
}
34+
35+
internal DotNetObjectRef(DotNetObjectRefManager referenceManager, long objectId, TValue value)
2536
{
26-
ObjectId = objectId;
27-
Value = value;
37+
_referenceManager = referenceManager;
38+
_objectId = objectId;
39+
_value = value;
2840
}
2941

3042
/// <summary>
3143
/// Gets the object instance represented by this wrapper.
3244
/// </summary>
33-
public TValue Value { get; }
45+
public TValue Value
46+
{
47+
get
48+
{
49+
ThrowIfDisposed();
50+
return _value;
51+
}
52+
}
3453

35-
internal long ObjectId { get; }
54+
internal long ObjectId
55+
{
56+
get
57+
{
58+
ThrowIfDisposed();
59+
return _objectId;
60+
}
61+
}
62+
63+
object IDotNetObjectRef.Value => Value;
64+
65+
internal bool Disposed { get; private set; }
3666

3767
/// <summary>
3868
/// Stops tracking this object reference, allowing it to be garbage collected
@@ -41,7 +71,19 @@ internal DotNetObjectRef(long objectId, TValue value)
4171
/// </summary>
4272
public void Dispose()
4373
{
44-
DotNetObjectRefManager.Current.ReleaseDotNetObject(ObjectId);
74+
if (!Disposed)
75+
{
76+
Disposed = true;
77+
_referenceManager.ReleaseDotNetObject(_objectId);
78+
}
79+
}
80+
81+
private void ThrowIfDisposed()
82+
{
83+
if (Disposed)
84+
{
85+
throw new ObjectDisposedException(GetType().Name);
86+
}
4587
}
4688
}
4789
}

src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ public override DotNetObjectRef<TValue> Read(ref Utf8JsonReader reader, Type typ
4040
throw new JsonException($"Required property {DotNetObjectRefKey} not found.");
4141
}
4242

43-
var value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
44-
return new DotNetObjectRef<TValue>(dotNetObjectId, value);
43+
var referenceManager = DotNetObjectRefManager.Current;
44+
return (DotNetObjectRef<TValue>)referenceManager.FindDotNetObject(dotNetObjectId);
4545
}
4646

4747
public override void Write(Utf8JsonWriter writer, DotNetObjectRef<TValue> value, JsonSerializerOptions options)

src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ namespace Microsoft.JSInterop
77
{
88
internal interface IDotNetObjectRef : IDisposable
99
{
10+
object Value { get; }
1011
}
1112
}

src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime =>
142142
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _));
143143

144144
Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property));
145-
var resultDto2 = Assert.IsType<TestDTO>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64()));
145+
var resultDto2 = Assert.IsType<DotNetObjectRef<TestDTO>>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64())).Value;
146146
Assert.Equal("MY STRING", resultDto2.StringVal);
147147
Assert.Equal(1299, resultDto2.IntVal);
148148
});
@@ -202,6 +202,20 @@ public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime =>
202202
Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid);
203203
});
204204

205+
[Fact]
206+
public Task DotNetObjectReferencesCanBeDisposed() => WithJSRuntime(jsRuntime =>
207+
{
208+
// Arrange
209+
var targetInstance = new SomePublicType();
210+
var objectRef = DotNetObjectRef.Create(targetInstance);
211+
212+
// Act
213+
DotNetDispatcher.BeginInvoke(null, null, "__Dispose", objectRef.ObjectId, null);
214+
215+
// Assert
216+
Assert.True(objectRef.Disposed);
217+
});
218+
205219
[Fact]
206220
public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime =>
207221
{
@@ -230,7 +244,7 @@ public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(
230244
var targetInstance = new SomePublicType();
231245
var objectRef = DotNetObjectRef.Create(targetInstance);
232246
jsRuntime.Invoke<object>("unimportant", objectRef);
233-
DotNetDispatcher.ReleaseDotNetObject(1);
247+
objectRef.Dispose();
234248

235249
// Act/Assert
236250
var ex = Assert.Throws<ArgumentException>(
@@ -320,7 +334,7 @@ public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime =>
320334

321335
// Assert
322336
Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson);
323-
var resultDto = (TestDTO)jsRuntime.ObjectRefManager.FindDotNetObject(3);
337+
var resultDto = ((DotNetObjectRef<TestDTO>)jsRuntime.ObjectRefManager.FindDotNetObject(3)).Value;
324338
Assert.Equal(1235, resultDto.IntVal);
325339
Assert.Equal("MY STRING", resultDto.StringVal);
326340
});

0 commit comments

Comments
 (0)