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 resultPromise = new Promise<T>((resolve, reject) => {
pendingAsyncCalls[asyncCallId] = { resolve, reject };
@ -269,10 +273,7 @@ module DotNet {
}
public dispose() {
const promise = invokeMethodAsync<any>(
'Microsoft.JSInterop',
'DotNetDispatcher.ReleaseDotNetObject',
this._id);
const promise = invokePossibleInstanceMethodAsync<any>(null, '__Dispose', this._id);
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 EndInvoke(string arguments) { }
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
{
@ -18,7 +16,7 @@ namespace Microsoft.JSInterop
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
{
internal DotNetObjectRef() { }
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public TValue Value { get { throw null; } }
public void Dispose() { }
}
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 EndInvoke(string arguments) { }
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
{
@ -18,7 +16,7 @@ namespace Microsoft.JSInterop
public sealed partial class DotNetObjectRef<TValue> : System.IDisposable where TValue : class
{
internal DotNetObjectRef() { }
public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public TValue Value { get { throw null; } }
public void Dispose() { }
}
public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime

View File

@ -18,6 +18,7 @@ namespace Microsoft.JSInterop
/// </summary>
public static class DotNetDispatcher
{
private const string DisposeDotNetObjectReferenceMethodName = "__Dispose";
internal static readonly JsonEncodedText DotNetObjectRefKey = JsonEncodedText.Encode("__dotNetObject");
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,
// because there would be nobody to police that. This method *is* the police.
var targetInstance = (object)null;
IDotNetObjectRef targetInstance = default;
if (dotNetObjectId != default)
{
targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
@ -78,7 +79,7 @@ namespace Microsoft.JSInterop
// original stack traces.
object syncResult = null;
ExceptionDispatchInfo syncException = null;
object targetInstance = null;
IDotNetObjectRef targetInstance = null;
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;
if (targetInstance != null)
if (objectReference is null)
{
assemblyKey = new AssemblyKey(assemblyName);
}
else
{
if (assemblyName != null)
{
throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'.");
}
assemblyKey = new AssemblyKey(targetInstance.GetType().Assembly);
}
else
{
assemblyKey = new AssemblyKey(assemblyName);
if (string.Equals(DisposeDotNetObjectReferenceMethodName, methodIdentifier, StringComparison.Ordinal))
{
// The client executed dotNetObjectReference.dispose(). Dispose the reference and exit.
objectReference.Dispose();
return default;
}
assemblyKey = new AssemblyKey(objectReference.Value.GetType().Assembly);
}
var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier);
@ -150,7 +158,8 @@ namespace Microsoft.JSInterop
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
{
@ -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)
{
if (string.IsNullOrWhiteSpace(assemblyKey.AssemblyName))

View File

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

View File

@ -10,7 +10,7 @@ namespace Microsoft.JSInterop
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 readonly ConcurrentDictionary<long, object> _trackedRefsById = new ConcurrentDictionary<long, object>();
private readonly ConcurrentDictionary<long, IDotNetObjectRef> _trackedRefsById = new ConcurrentDictionary<long, IDotNetObjectRef>();
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);
_trackedRefsById[dotNetObjectId] = dotNetObjectRef;
@ -33,7 +33,7 @@ namespace Microsoft.JSInterop
return dotNetObjectId;
}
public object FindDotNetObject(long dotNetObjectId)
public IDotNetObjectRef FindDotNetObject(long dotNetObjectId)
{
return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef)
? dotNetObjectRef

View File

@ -16,23 +16,53 @@ namespace Microsoft.JSInterop
[JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))]
public sealed class DotNetObjectRef<TValue> : IDotNetObjectRef, IDisposable where TValue : class
{
private readonly DotNetObjectRefManager _referenceManager;
private readonly TValue _value;
private readonly long _objectId;
/// <summary>
/// Initializes a new instance of <see cref="DotNetObjectRef{TValue}" />.
/// </summary>
/// <param name="objectId">The object Id.</param>
/// <param name="referenceManager"></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;
Value = value;
_referenceManager = referenceManager;
_objectId = _referenceManager.TrackObject(this);
_value = value;
}
internal DotNetObjectRef(DotNetObjectRefManager referenceManager, long objectId, TValue value)
{
_referenceManager = referenceManager;
_objectId = objectId;
_value = value;
}
/// <summary>
/// Gets the object instance represented by this wrapper.
/// </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>
/// Stops tracking this object reference, allowing it to be garbage collected
@ -41,7 +71,19 @@ namespace Microsoft.JSInterop
/// </summary>
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.");
}
var value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
return new DotNetObjectRef<TValue>(dotNetObjectId, value);
var referenceManager = DotNetObjectRefManager.Current;
return (DotNetObjectRef<TValue>)referenceManager.FindDotNetObject(dotNetObjectId);
}
public override void Write(Utf8JsonWriter writer, DotNetObjectRef<TValue> value, JsonSerializerOptions options)

View File

@ -7,5 +7,6 @@ namespace Microsoft.JSInterop
{
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.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(1299, resultDto2.IntVal);
});
@ -202,6 +202,20 @@ namespace Microsoft.JSInterop
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]
public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime =>
{
@ -230,7 +244,7 @@ namespace Microsoft.JSInterop
var targetInstance = new SomePublicType();
var objectRef = DotNetObjectRef.Create(targetInstance);
jsRuntime.Invoke<object>("unimportant", objectRef);
DotNetDispatcher.ReleaseDotNetObject(1);
objectRef.Dispose();
// Act/Assert
var ex = Assert.Throws<ArgumentException>(
@ -320,7 +334,7 @@ namespace Microsoft.JSInterop
// Assert
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("MY STRING", resultDto.StringVal);
});

View File

@ -24,10 +24,11 @@ namespace Microsoft.JSInterop
var objRef = DotNetObjectRef.Create(new object());
// Act
Assert.Equal(1, objRef.ObjectId);
objRef.Dispose();
// 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);
});
}

View File

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