Special case Disposing DotNetObjectReferences (dotnet/extensions#2176)

* Special case Disposing DotNetObjectReferences

This removes a public JSInvokable method required for disposing DotNetObjectReferences


\n\nCommit migrated from d6bfc28e21
This commit is contained in:
Pranav K 2019-08-14 09:44:33 -07:00 committed by GitHub
parent 60778e8dc7
commit edd5f54bc3
13 changed files with 109 additions and 62 deletions

View File

@ -66,7 +66,11 @@ module DotNet {
} }
} }
function invokePossibleInstanceMethodAsync<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): Promise<T> { function invokePossibleInstanceMethodAsync<T>(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, ...args: any[]): Promise<T> {
if (assemblyName && dotNetObjectId) {
throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ;
}
const asyncCallId = nextAsyncCallId++; const asyncCallId = nextAsyncCallId++;
const resultPromise = new Promise<T>((resolve, reject) => { const resultPromise = new Promise<T>((resolve, reject) => {
pendingAsyncCalls[asyncCallId] = { resolve, reject }; pendingAsyncCalls[asyncCallId] = { resolve, reject };
@ -269,10 +273,7 @@ module DotNet {
} }
public dispose() { public dispose() {
const promise = invokeMethodAsync<any>( const promise = invokePossibleInstanceMethodAsync<any>(null, '__Dispose', this._id);
'Microsoft.JSInterop',
'DotNetDispatcher.ReleaseDotNetObject',
this._id);
promise.catch(error => console.error(error)); promise.catch(error => console.error(error));
} }

View File

@ -8,8 +8,6 @@ namespace Microsoft.JSInterop
public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
public static void EndInvoke(string arguments) { } public static void EndInvoke(string arguments) { }
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
[Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")]
public static void ReleaseDotNetObject(long dotNetObjectId) { }
} }
public static partial class DotNetObjectRef public static partial class DotNetObjectRef
{ {
@ -18,7 +16,7 @@ namespace Microsoft.JSInterop
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
{ {
internal DotNetObjectRef() { } internal DotNetObjectRef() { }
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public TValue Value { get { throw null; } }
public void Dispose() { } public void Dispose() { }
} }
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime

View File

@ -8,8 +8,6 @@ namespace Microsoft.JSInterop
public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { }
public static void EndInvoke(string arguments) { } public static void EndInvoke(string arguments) { }
public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; }
[Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")]
public static void ReleaseDotNetObject(long dotNetObjectId) { }
} }
public static partial class DotNetObjectRef public static partial class DotNetObjectRef
{ {
@ -18,7 +16,7 @@ namespace Microsoft.JSInterop
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
{ {
internal DotNetObjectRef() { } internal DotNetObjectRef() { }
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public TValue Value { get { throw null; } }
public void Dispose() { } public void Dispose() { }
} }
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime

View File

@ -18,6 +18,7 @@ namespace Microsoft.JSInterop
/// </summary> /// </summary>
public static class DotNetDispatcher public static class DotNetDispatcher
{ {
private const string DisposeDotNetObjectReferenceMethodName = "__Dispose";
internal static readonly JsonEncodedText DotNetObjectRefKey = JsonEncodedText.Encode("__dotNetObject"); internal static readonly JsonEncodedText DotNetObjectRefKey = JsonEncodedText.Encode("__dotNetObject");
private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly private static readonly ConcurrentDictionary<AssemblyKey, IReadOnlyDictionary<string, (MethodInfo, Type[])>> _cachedMethodsByAssembly
@ -38,7 +39,7 @@ namespace Microsoft.JSInterop
// the targeted method has [JSInvokable]. It is not itself subject to that restriction, // the targeted method has [JSInvokable]. It is not itself subject to that restriction,
// because there would be nobody to police that. This method *is* the police. // because there would be nobody to police that. This method *is* the police.
var targetInstance = (object)null; IDotNetObjectRef targetInstance = default;
if (dotNetObjectId != default) if (dotNetObjectId != default)
{ {
targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
@ -78,7 +79,7 @@ namespace Microsoft.JSInterop
// original stack traces. // original stack traces.
object syncResult = null; object syncResult = null;
ExceptionDispatchInfo syncException = null; ExceptionDispatchInfo syncException = null;
object targetInstance = null; IDotNetObjectRef targetInstance = null;
try try
{ {
@ -127,21 +128,28 @@ namespace Microsoft.JSInterop
} }
} }
private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson) private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectRef objectReference, string argsJson)
{ {
AssemblyKey assemblyKey; AssemblyKey assemblyKey;
if (targetInstance != null) if (objectReference is null)
{
assemblyKey = new AssemblyKey(assemblyName);
}
else
{ {
if (assemblyName != null) if (assemblyName != null)
{ {
throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'."); throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'.");
} }
assemblyKey = new AssemblyKey(targetInstance.GetType().Assembly); if (string.Equals(DisposeDotNetObjectReferenceMethodName, methodIdentifier, StringComparison.Ordinal))
} {
else // The client executed dotNetObjectReference.dispose(). Dispose the reference and exit.
{ objectReference.Dispose();
assemblyKey = new AssemblyKey(assemblyName); return default;
}
assemblyKey = new AssemblyKey(objectReference.Value.GetType().Assembly);
} }
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier); var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
@ -150,7 +158,8 @@ namespace Microsoft.JSInterop
try try
{ {
return methodInfo.Invoke(targetInstance, suppliedArgs); // objectReference will be null if this call invokes a static JSInvokable method.
return methodInfo.Invoke(objectReference?.Value, suppliedArgs);
} }
catch (TargetInvocationException tie) // Avoid using exception filters for AOT runtime support catch (TargetInvocationException tie) // Avoid using exception filters for AOT runtime support
{ {
@ -280,22 +289,6 @@ namespace Microsoft.JSInterop
} }
} }
/// <summary>
/// Releases the reference to the specified .NET object. This allows the .NET runtime
/// to garbage collect that object if there are no other references to it.
///
/// To avoid leaking memory, the JavaScript side code must call this for every .NET
/// object it obtains a reference to. The exception is if that object is used for
/// the entire lifetime of a given user's session, in which case it is released
/// automatically when the JavaScript runtime is disposed.
/// </summary>
/// <param name="dotNetObjectId">The identifier previously passed to JavaScript code.</param>
[JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))]
public static void ReleaseDotNetObject(long dotNetObjectId)
{
DotNetObjectRefManager.Current.ReleaseDotNetObject(dotNetObjectId);
}
private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, string methodIdentifier) private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, string methodIdentifier)
{ {
if (string.IsNullOrWhiteSpace(assemblyKey.AssemblyName)) if (string.IsNullOrWhiteSpace(assemblyKey.AssemblyName))

View File

@ -15,8 +15,7 @@ namespace Microsoft.JSInterop
/// <returns>An instance of <see cref="DotNetObjectRef{TValue}" />.</returns> /// <returns>An instance of <see cref="DotNetObjectRef{TValue}" />.</returns>
public static DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class public static DotNetObjectRef<TValue> Create<TValue>(TValue value) where TValue : class
{ {
var objectId = DotNetObjectRefManager.Current.TrackObject(value); return new DotNetObjectRef<TValue>(DotNetObjectRefManager.Current, value);
return new DotNetObjectRef<TValue>(objectId, value);
} }
} }
} }

View File

@ -10,7 +10,7 @@ namespace Microsoft.JSInterop
internal class DotNetObjectRefManager internal class DotNetObjectRefManager
{ {
private long _nextId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1 private long _nextId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1
private readonly ConcurrentDictionary<long, object> _trackedRefsById = new ConcurrentDictionary<long, object>(); private readonly ConcurrentDictionary<long, IDotNetObjectRef> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectRef>();
public static DotNetObjectRefManager Current public static DotNetObjectRefManager Current
{ {
@ -25,7 +25,7 @@ namespace Microsoft.JSInterop
} }
} }
public long TrackObject(object dotNetObjectRef) public long TrackObject(IDotNetObjectRef dotNetObjectRef)
{ {
var dotNetObjectId = Interlocked.Increment(ref _nextId); var dotNetObjectId = Interlocked.Increment(ref _nextId);
_trackedRefsById[dotNetObjectId] = dotNetObjectRef; _trackedRefsById[dotNetObjectId] = dotNetObjectRef;
@ -33,7 +33,7 @@ namespace Microsoft.JSInterop
return dotNetObjectId; return dotNetObjectId;
} }
public object FindDotNetObject(long dotNetObjectId) public IDotNetObjectRef FindDotNetObject(long dotNetObjectId)
{ {
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
? dotNetObjectRef ? dotNetObjectRef

View File

@ -16,23 +16,53 @@ namespace Microsoft.JSInterop
[JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))] [JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))]
public sealed class DotNetObjectRef<TValue> : IDotNetObjectRef, IDisposable where TValue : class public sealed class DotNetObjectRef<TValue> : IDotNetObjectRef, IDisposable where TValue : class
{ {
private readonly DotNetObjectRefManager _referenceManager;
private readonly TValue _value;
private readonly long _objectId;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="DotNetObjectRef{TValue}" />. /// Initializes a new instance of <see cref="DotNetObjectRef{TValue}" />.
/// </summary> /// </summary>
/// <param name="objectId">The object Id.</param> /// <param name="referenceManager"></param>
/// <param name="value">The value to pass by reference.</param> /// <param name="value">The value to pass by reference.</param>
internal DotNetObjectRef(long objectId, TValue value) internal DotNetObjectRef(DotNetObjectRefManager referenceManager, TValue value)
{ {
ObjectId = objectId; _referenceManager = referenceManager;
Value = value; _objectId = _referenceManager.TrackObject(this);
_value = value;
}
internal DotNetObjectRef(DotNetObjectRefManager referenceManager, long objectId, TValue value)
{
_referenceManager = referenceManager;
_objectId = objectId;
_value = value;
} }
/// <summary> /// <summary>
/// Gets the object instance represented by this wrapper. /// Gets the object instance represented by this wrapper.
/// </summary> /// </summary>
public TValue Value { get; } public TValue Value
{
get
{
ThrowIfDisposed();
return _value;
}
}
internal long ObjectId { get; } internal long ObjectId
{
get
{
ThrowIfDisposed();
return _objectId;
}
}
object IDotNetObjectRef.Value => Value;
internal bool Disposed { get; private set; }
/// <summary> /// <summary>
/// Stops tracking this object reference, allowing it to be garbage collected /// Stops tracking this object reference, allowing it to be garbage collected
@ -41,7 +71,19 @@ namespace Microsoft.JSInterop
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
DotNetObjectRefManager.Current.ReleaseDotNetObject(ObjectId); if (!Disposed)
{
Disposed = true;
_referenceManager.ReleaseDotNetObject(_objectId);
}
}
private void ThrowIfDisposed()
{
if (Disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
} }
} }
} }

View File

@ -40,8 +40,8 @@ namespace Microsoft.JSInterop
throw new JsonException($"Required property {DotNetObjectRefKey} not found."); throw new JsonException($"Required property {DotNetObjectRefKey} not found.");
} }
var value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); var referenceManager = DotNetObjectRefManager.Current;
return new DotNetObjectRef<TValue>(dotNetObjectId, value); return (DotNetObjectRef<TValue>)referenceManager.FindDotNetObject(dotNetObjectId);
} }
public override void Write(Utf8JsonWriter writer, DotNetObjectRef<TValue> value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, DotNetObjectRef<TValue> value, JsonSerializerOptions options)

View File

@ -7,5 +7,6 @@ namespace Microsoft.JSInterop
{ {
internal interface IDotNetObjectRef : IDisposable internal interface IDotNetObjectRef : IDisposable
{ {
object Value { get; }
} }
} }

View File

@ -142,7 +142,7 @@ namespace Microsoft.JSInterop
Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _)); Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _));
Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property)); Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property));
var resultDto2 = Assert.IsType<TestDTO>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64())); var resultDto2 = Assert.IsType<DotNetObjectRef<TestDTO>>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64())).Value;
Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal("MY STRING", resultDto2.StringVal);
Assert.Equal(1299, resultDto2.IntVal); Assert.Equal(1299, resultDto2.IntVal);
}); });
@ -202,6 +202,20 @@ namespace Microsoft.JSInterop
Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid); Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid);
}); });
[Fact]
public Task DotNetObjectReferencesCanBeDisposed() => WithJSRuntime(jsRuntime =>
{
// Arrange
var targetInstance = new SomePublicType();
var objectRef = DotNetObjectRef.Create(targetInstance);
// Act
DotNetDispatcher.BeginInvoke(null, null, "__Dispose", objectRef.ObjectId, null);
// Assert
Assert.True(objectRef.Disposed);
});
[Fact] [Fact]
public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime => public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime =>
{ {
@ -230,7 +244,7 @@ namespace Microsoft.JSInterop
var targetInstance = new SomePublicType(); var targetInstance = new SomePublicType();
var objectRef = DotNetObjectRef.Create(targetInstance); var objectRef = DotNetObjectRef.Create(targetInstance);
jsRuntime.Invoke<object>("unimportant", objectRef); jsRuntime.Invoke<object>("unimportant", objectRef);
DotNetDispatcher.ReleaseDotNetObject(1); objectRef.Dispose();
// Act/Assert // Act/Assert
var ex = Assert.Throws<ArgumentException>( var ex = Assert.Throws<ArgumentException>(
@ -320,7 +334,7 @@ namespace Microsoft.JSInterop
// Assert // Assert
Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson); Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson);
var resultDto = (TestDTO)jsRuntime.ObjectRefManager.FindDotNetObject(3); var resultDto = ((DotNetObjectRef<TestDTO>)jsRuntime.ObjectRefManager.FindDotNetObject(3)).Value;
Assert.Equal(1235, resultDto.IntVal); Assert.Equal(1235, resultDto.IntVal);
Assert.Equal("MY STRING", resultDto.StringVal); Assert.Equal("MY STRING", resultDto.StringVal);
}); });

View File

@ -24,10 +24,11 @@ namespace Microsoft.JSInterop
var objRef = DotNetObjectRef.Create(new object()); var objRef = DotNetObjectRef.Create(new object());
// Act // Act
Assert.Equal(1, objRef.ObjectId);
objRef.Dispose(); objRef.Dispose();
// Assert // Assert
var ex = Assert.Throws<ArgumentException>(() => jsRuntime.ObjectRefManager.FindDotNetObject(objRef.ObjectId)); var ex = Assert.Throws<ArgumentException>(() => jsRuntime.ObjectRefManager.FindDotNetObject(1));
Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message);
}); });
} }

View File

@ -60,9 +60,9 @@ namespace Microsoft.JSInterop.Tests
Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3}}]", call.ArgsJson); Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3}}]", call.ArgsJson);
// Assert: Objects were tracked // Assert: Objects were tracked
Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1)); Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1).Value);
Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2)); Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2).Value);
Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3)); Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3).Value);
} }
[Fact] [Fact]

View File

@ -282,10 +282,10 @@ namespace Microsoft.JSInterop
Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":3},\"obj3\":{\"__dotNetObject\":4},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":2}}]", call.ArgsJson); Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":3},\"obj3\":{\"__dotNetObject\":4},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":2}}]", call.ArgsJson);
// Assert: Objects were tracked // Assert: Objects were tracked
Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1)); Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1).Value);
Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2)); Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2).Value);
Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3)); Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3).Value);
Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4)); Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4).Value);
} }
[Fact] [Fact]