diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index 30a91bde4d..3af355f6d0 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -55,7 +55,7 @@ module DotNet { return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args); } - function invokePossibleInstanceMethod(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T { + function invokePossibleInstanceMethod(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): T { const dispatcher = getRequiredDispatcher(); if (dispatcher.invokeDotNetFromJS) { const argsJson = JSON.stringify(args, argReplacer); @@ -66,7 +66,7 @@ module DotNet { } } - function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, ...args: any[]): Promise { + function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): Promise { if (assemblyName && dotNetObjectId) { throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ; } @@ -273,7 +273,7 @@ module DotNet { } public dispose() { - const promise = invokePossibleInstanceMethodAsync(null, '__Dispose', this._id); + const promise = invokePossibleInstanceMethodAsync(null, '__Dispose', this._id, null); promise.catch(error => console.error(error)); } diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs index a5fbbc768a..953f8b0329 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs @@ -48,11 +48,11 @@ namespace Microsoft.JSInterop { protected JSRuntime() { } protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId); public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, object[] args) { throw null; } public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; } - public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } } public static partial class JSRuntimeExtensions { @@ -72,8 +72,8 @@ namespace Microsoft.JSInterop.Infrastructure { public static partial class DotNetDispatcher { - public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } - public static void EndInvokeJS(string arguments) { } - public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } } } diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs index a5fbbc768a..953f8b0329 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -48,11 +48,11 @@ namespace Microsoft.JSInterop { protected JSRuntime() { } protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId); public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, object[] args) { throw null; } public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; } - public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } } public static partial class JSRuntimeExtensions { @@ -72,8 +72,8 @@ namespace Microsoft.JSInterop.Infrastructure { public static partial class DotNetDispatcher { - public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } - public static void EndInvokeJS(string arguments) { } - public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs index 24b13f0c85..989d8062bb 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.JSInterop.Infrastructure; - namespace Microsoft.JSInterop { /// @@ -17,7 +15,7 @@ namespace Microsoft.JSInterop /// An instance of . public static DotNetObjectReference Create(TValue value) where TValue : class { - return new DotNetObjectReference(DotNetObjectReferenceManager.Current, value); + return new DotNetObjectReference(value); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs index eb1ac6a234..773c2ed9a3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text.Json.Serialization; +using System.Diagnostics; using Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop @@ -14,22 +14,18 @@ namespace Microsoft.JSInterop /// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code. /// /// The type of the value to wrap. - [JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))] public sealed class DotNetObjectReference : IDotNetObjectReference, IDisposable where TValue : class { - private readonly DotNetObjectReferenceManager _referenceManager; private readonly TValue _value; - private readonly long _objectId; + private long _objectId; + private JSRuntime _jsRuntime; /// /// Initializes a new instance of . /// - /// /// The value to pass by reference. - internal DotNetObjectReference(DotNetObjectReferenceManager referenceManager, TValue value) + internal DotNetObjectReference(TValue value) { - _referenceManager = referenceManager; - _objectId = _referenceManager.TrackObject(this); _value = value; } @@ -50,8 +46,30 @@ namespace Microsoft.JSInterop get { ThrowIfDisposed(); + Debug.Assert(_objectId != 0, "Accessing ObjectId without tracking is always incorrect."); + return _objectId; } + set + { + ThrowIfDisposed(); + _objectId = value; + } + } + + internal JSRuntime JSRuntime + { + get + { + ThrowIfDisposed(); + return _jsRuntime; + } + set + { + ThrowIfDisposed(); + _jsRuntime = value; + } + } object IDotNetObjectReference.Value => Value; @@ -68,11 +86,15 @@ namespace Microsoft.JSInterop if (!Disposed) { Disposed = true; - _referenceManager.ReleaseDotNetObject(_objectId); + + if (_jsRuntime != null) + { + _jsRuntime.ReleaseObjectReference(_objectId); + } } } - private void ThrowIfDisposed() + internal void ThrowIfDisposed() { if (Disposed) { diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index d4a4de14dd..92afc6278d 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -27,12 +27,13 @@ namespace Microsoft.JSInterop.Infrastructure /// /// Receives a call from JS to .NET, locating and invoking the specified method. /// + /// The . /// The assembly containing the method to be invoked. /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. /// For instance method calls, identifies the target object. /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static string Invoke(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { // This method doesn't need [JSInvokable] because the platform is responsible for having // some way to dispatch calls here. The logic inside here is the thing that checks whether @@ -42,41 +43,38 @@ namespace Microsoft.JSInterop.Infrastructure IDotNetObjectReference targetInstance = default; if (dotNetObjectId != default) { - targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(dotNetObjectId); } - var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + var syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson); if (syncResult == null) { return null; } - return JsonSerializer.Serialize(syncResult, JsonSerializerOptionsProvider.Options); + return JsonSerializer.Serialize(syncResult, jsRuntime.JsonSerializerOptions); } /// /// Receives a call from JS to .NET, locating and invoking the specified method asynchronously. /// + /// The . /// A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required. /// The assembly containing the method to be invoked. /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. /// For instance method calls, identifies the target object. /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static void BeginInvokeDotNet(JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { // This method doesn't need [JSInvokable] because the platform is responsible for having // some way to dispatch calls here. The logic inside here is the thing that checks whether // 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. - // DotNetDispatcher only works with JSRuntimeBase instances. - // If the developer wants to use a totally custom IJSRuntime, then their JS-side - // code has to implement its own way of returning async results. - var jsRuntimeBaseInstance = (JSRuntime)JSRuntime.Current; - // Using ExceptionDispatchInfo here throughout because we want to always preserve // original stack traces. + object syncResult = null; ExceptionDispatchInfo syncException = null; IDotNetObjectReference targetInstance = null; @@ -85,10 +83,10 @@ namespace Microsoft.JSInterop.Infrastructure { if (dotNetObjectId != default) { - targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(dotNetObjectId); } - syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson); } catch (Exception ex) { @@ -103,7 +101,7 @@ namespace Microsoft.JSInterop.Infrastructure else if (syncException != null) { // Threw synchronously, let's respond. - jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId); } else if (syncResult is Task task) { @@ -115,20 +113,20 @@ namespace Microsoft.JSInterop.Infrastructure { var exception = t.Exception.GetBaseException(); - jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId); } var result = TaskGenericsUtil.GetTaskResult(task); - jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId); }, TaskScheduler.Current); } else { - jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId); } } - private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson) + private static object InvokeSynchronously(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson) { AssemblyKey assemblyKey; if (objectReference is null) @@ -154,7 +152,7 @@ namespace Microsoft.JSInterop.Infrastructure var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier); - var suppliedArgs = ParseArguments(methodIdentifier, argsJson, parameterTypes); + var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes); try { @@ -173,7 +171,7 @@ namespace Microsoft.JSInterop.Infrastructure } } - internal static object[] ParseArguments(string methodIdentifier, string arguments, Type[] parameterTypes) + internal static object[] ParseArguments(JSRuntime jsRuntime, string methodIdentifier, string arguments, Type[] parameterTypes) { if (parameterTypes.Length == 0) { @@ -198,7 +196,7 @@ namespace Microsoft.JSInterop.Infrastructure 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."); } - suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, JsonSerializerOptionsProvider.Options); + suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, jsRuntime.JsonSerializerOptions); index++; } @@ -247,18 +245,13 @@ namespace Microsoft.JSInterop.Infrastructure /// method is responsible for handling any possible exception generated from the arguments /// passed in as parameters. /// + /// The . /// The serialized arguments for the callback completion. /// /// This method can throw any exception either from the argument received or as a result /// of executing any callback synchronously upon completion. /// - public static void EndInvokeJS(string arguments) - { - var jsRuntimeBase = (JSRuntime)JSRuntime.Current; - ParseEndInvokeArguments(jsRuntimeBase, arguments); - } - - internal static void ParseEndInvokeArguments(JSRuntime jsRuntimeBase, string arguments) + public static void EndInvokeJS(JSRuntime jsRuntime, string arguments) { var utf8JsonBytes = Encoding.UTF8.GetBytes(arguments); @@ -281,7 +274,7 @@ namespace Microsoft.JSInterop.Infrastructure var success = reader.GetBoolean(); reader.Read(); - jsRuntimeBase.EndInvokeJS(taskId, success, ref reader); + jsRuntime.EndInvokeJS(taskId, success, ref reader); if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray) { diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs index c077ac0b17..7658bbc2c3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs @@ -9,8 +9,15 @@ namespace Microsoft.JSInterop.Infrastructure { internal sealed class DotNetObjectReferenceJsonConverter : JsonConverter> where TValue : class { + public DotNetObjectReferenceJsonConverter(JSRuntime jsRuntime) + { + JSRuntime = jsRuntime; + } + private static JsonEncodedText DotNetObjectRefKey => DotNetDispatcher.DotNetObjectRefKey; + public JSRuntime JSRuntime { get; } + public override DotNetObjectReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { long dotNetObjectId = 0; @@ -40,14 +47,16 @@ namespace Microsoft.JSInterop.Infrastructure throw new JsonException($"Required property {DotNetObjectRefKey} not found."); } - var referenceManager = DotNetObjectReferenceManager.Current; - return (DotNetObjectReference)referenceManager.FindDotNetObject(dotNetObjectId); + var value = (DotNetObjectReference)JSRuntime.GetObjectReference(dotNetObjectId); + return value; } public override void Write(Utf8JsonWriter writer, DotNetObjectReference value, JsonSerializerOptions options) { + var objectId = JSRuntime.TrackObjectReference(value); + writer.WriteStartObject(); - writer.WriteNumber(DotNetObjectRefKey, value.ObjectId); + writer.WriteNumber(DotNetObjectRefKey, objectId); writer.WriteEndObject(); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs index 350530b624..288bfdd090 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs @@ -9,6 +9,13 @@ namespace Microsoft.JSInterop.Infrastructure { internal sealed class DotNetObjectReferenceJsonConverterFactory : JsonConverterFactory { + public DotNetObjectReferenceJsonConverterFactory(JSRuntime jsRuntime) + { + JSRuntime = jsRuntime; + } + + public JSRuntime JSRuntime { get; } + public override bool CanConvert(Type typeToConvert) { return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectReference<>); @@ -20,7 +27,7 @@ namespace Microsoft.JSInterop.Infrastructure var instanceType = typeToConvert.GetGenericArguments()[0]; var converterType = typeof(DotNetObjectReferenceJsonConverter<>).MakeGenericType(instanceType); - return (JsonConverter)Activator.CreateInstance(converterType); + return (JsonConverter)Activator.CreateInstance(converterType, JSRuntime); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs deleted file mode 100644 index 709dd963fa..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Threading; - -namespace Microsoft.JSInterop.Infrastructure -{ - internal class DotNetObjectReferenceManager - { - 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 _trackedRefsById = new ConcurrentDictionary(); - - public static DotNetObjectReferenceManager Current - { - get - { - if (!(JSRuntime.Current is JSRuntime jsRuntime)) - { - throw new InvalidOperationException("JSRuntime must be set up correctly and must be an instance of JSRuntimeBase to use DotNetObjectReference."); - } - - return jsRuntime.ObjectRefManager; - } - } - - public long TrackObject(IDotNetObjectReference dotNetObjectRef) - { - var dotNetObjectId = Interlocked.Increment(ref _nextId); - _trackedRefsById[dotNetObjectId] = dotNetObjectRef; - - return dotNetObjectId; - } - - public IDotNetObjectReference FindDotNetObject(long dotNetObjectId) - { - return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) - ? dotNetObjectRef - : throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectRef instance was already disposed.", nameof(dotNetObjectId)); - - } - - /// - /// Stops tracking the specified .NET object reference. - /// This may be invoked either by disposing a DotNetObjectRef in .NET code, or via JS interop by calling "dispose" on the corresponding instance in JavaScript code - /// - /// The ID of the . - public void ReleaseDotNetObject(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _); - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs index cf8cc7030b..2b96bbbbb5 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs @@ -19,13 +19,13 @@ namespace Microsoft.JSInterop /// An instance of obtained by JSON-deserializing the return value. public TValue Invoke(string identifier, params object[] args) { - var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options)); + var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptions)); if (resultJson is null) { return default; } - return JsonSerializer.Deserialize(resultJson, JsonSerializerOptionsProvider.Options); + return JsonSerializer.Deserialize(resultJson, JsonSerializerOptions); } /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 598b47c4d4..ba411b72db 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Linq; using System.Text.Json; using System.Threading; @@ -16,36 +17,40 @@ namespace Microsoft.JSInterop /// public abstract partial class JSRuntime : IJSRuntime { - private static readonly AsyncLocal _currentJSRuntime = new AsyncLocal(); - - internal static IJSRuntime Current => _currentJSRuntime.Value; - + private long _nextObjectReferenceId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1 private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" - private readonly ConcurrentDictionary _pendingTasks - = new ConcurrentDictionary(); - + private readonly ConcurrentDictionary _pendingTasks = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _trackedRefsById = new ConcurrentDictionary(); private readonly ConcurrentDictionary _cancellationRegistrations = new ConcurrentDictionary(); - internal DotNetObjectReferenceManager ObjectRefManager { get; } = new DotNetObjectReferenceManager(); + /// + /// Initializes a new instance of . + /// + protected JSRuntime() + { + JsonSerializerOptions = new JsonSerializerOptions + { + MaxDepth = 32, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + Converters = + { + new DotNetObjectReferenceJsonConverterFactory(this), + } + }; + } + + /// + /// Gets the used to serialize and deserialize interop payloads. + /// + protected internal JsonSerializerOptions JsonSerializerOptions { get; } /// /// Gets or sets the default timeout for asynchronous JavaScript calls. /// protected TimeSpan? DefaultAsyncTimeout { get; set; } - /// - /// Sets the current JS runtime to the supplied instance. - /// - /// This is intended for framework use. Developers should not normally need to call this method. - /// - /// The new current . - public static void SetCurrentJSRuntime(IJSRuntime instance) - { - _currentJSRuntime.Value = instance - ?? throw new ArgumentNullException(nameof(instance)); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -103,7 +108,7 @@ namespace Microsoft.JSInterop } var argsJson = args?.Any() == true ? - JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options) : + JsonSerializer.Serialize(args, JsonSerializerOptions) : null; BeginInvokeJS(taskId, identifier, argsJson); @@ -176,7 +181,7 @@ namespace Microsoft.JSInterop { var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); - var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptionsProvider.Options); + var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); } else @@ -191,5 +196,48 @@ namespace Microsoft.JSInterop TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception)); } } + + internal long TrackObjectReference(DotNetObjectReference dotNetObjectReference) where TValue : class + { + if (dotNetObjectReference == null) + { + throw new ArgumentNullException(nameof(dotNetObjectReference)); + } + + dotNetObjectReference.ThrowIfDisposed(); + + var jsRuntime = dotNetObjectReference.JSRuntime; + if (jsRuntime is null) + { + var dotNetObjectId = Interlocked.Increment(ref _nextObjectReferenceId); + + dotNetObjectReference.JSRuntime = this; + dotNetObjectReference.ObjectId = dotNetObjectId; + + _trackedRefsById[dotNetObjectId] = dotNetObjectReference; + } + else if (!ReferenceEquals(this, jsRuntime)) + { + throw new InvalidOperationException($"{dotNetObjectReference.GetType().Name} is already being tracked by a different instance of {nameof(JSRuntime)}." + + $" A common cause is caching an instance of {nameof(DotNetObjectReference)} globally. Consider creating instances of {nameof(DotNetObjectReference)} at the JSInterop callsite."); + } + + Debug.Assert(dotNetObjectReference.ObjectId != 0); + return dotNetObjectReference.ObjectId; + } + + internal IDotNetObjectReference GetObjectReference(long dotNetObjectId) + { + return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) + ? dotNetObjectRef + : throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectReference instance was already disposed.", nameof(dotNetObjectId)); + } + + /// + /// Stops tracking the specified .NET object reference. + /// This may be invoked either by disposing a DotNetObjectRef in .NET code, or via JS interop by calling "dispose" on the corresponding instance in JavaScript code + /// + /// The ID of the . + internal void ReleaseObjectReference(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs b/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs deleted file mode 100644 index 62244270e3..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Text.Json; - -namespace Microsoft.JSInterop -{ - internal static class JsonSerializerOptionsProvider - { - public static readonly JsonSerializerOptions Options = new JsonSerializerOptions - { - MaxDepth = 32, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - }; - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs index bcd5c95028..95fad485a7 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs @@ -2,34 +2,102 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Threading.Tasks; using Xunit; -using static Microsoft.JSInterop.TestJSRuntime; namespace Microsoft.JSInterop { public class DotNetObjectReferenceTest { [Fact] - public Task CanAccessValue() => WithJSRuntime(_ => + public void CanAccessValue() { var obj = new object(); Assert.Same(obj, DotNetObjectReference.Create(obj).Value); - }); + } [Fact] - public Task NotifiesAssociatedJsRuntimeOfDisposal() => WithJSRuntime(jsRuntime => + public void TrackObjectReference_AssignsObjectId() { // Arrange + var jsRuntime = new TestJSRuntime(); var objRef = DotNetObjectReference.Create(new object()); // Act + var objectId = jsRuntime.TrackObjectReference(objRef); + + // Act + Assert.Equal(objectId, objRef.ObjectId); Assert.Equal(1, objRef.ObjectId); + } + + [Fact] + public void TrackObjectReference_AllowsMultipleCallsUsingTheSameJSRuntime() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + var objRef = DotNetObjectReference.Create(new object()); + + // Act + var objectId1 = jsRuntime.TrackObjectReference(objRef); + var objectId2 = jsRuntime.TrackObjectReference(objRef); + + // Act + Assert.Equal(objectId1, objectId2); + } + + [Fact] + public void TrackObjectReference_ThrowsIfDifferentJSRuntimeInstancesAreUsed() + { + // Arrange + var objRef = DotNetObjectReference.Create("Hello world"); + var expected = $"{objRef.GetType().Name} is already being tracked by a different instance of {nameof(JSRuntime)}. A common cause is caching an instance of {nameof(DotNetObjectReference)}" + + $" globally. Consider creating instances of {nameof(DotNetObjectReference)} at the JSInterop callsite."; + var jsRuntime1 = new TestJSRuntime(); + var jsRuntime2 = new TestJSRuntime(); + jsRuntime1.TrackObjectReference(objRef); + + // Act + var ex = Assert.Throws(() => jsRuntime2.TrackObjectReference(objRef)); + + // Assert + Assert.Equal(expected, ex.Message); + } + + [Fact] + public void Dispose_StopsTrackingObject() + { + // Arrange + var objRef = DotNetObjectReference.Create("Hello world"); + var jsRuntime = new TestJSRuntime(); + jsRuntime.TrackObjectReference(objRef); + var objectId = objRef.ObjectId; + var expected = $"There is no tracked object with id '{objectId}'. Perhaps the DotNetObjectReference instance was already disposed."; + + // Act + Assert.Same(objRef, jsRuntime.GetObjectReference(objectId)); objRef.Dispose(); // Assert - var ex = Assert.Throws(() => jsRuntime.ObjectRefManager.FindDotNetObject(1)); - Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); - }); + Assert.True(objRef.Disposed); + Assert.Throws(() => jsRuntime.GetObjectReference(objectId)); + } + + [Fact] + public void DoubleDispose_Works() + { + // Arrange + var objRef = DotNetObjectReference.Create("Hello world"); + var jsRuntime = new TestJSRuntime(); + jsRuntime.TrackObjectReference(objRef); + var objectId = objRef.ObjectId; + + // Act + Assert.Same(objRef, jsRuntime.GetObjectReference(objectId)); + objRef.Dispose(); + + // Assert + objRef.Dispose(); + // If we got this far, this did not throw. + } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index d9ddac2a89..2d8208b7a6 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs @@ -20,7 +20,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]"); + DotNetDispatcher.Invoke(new TestJSRuntime(), " ", "SomeMethod", default, "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -32,7 +32,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]"); + DotNetDispatcher.Invoke(new TestJSRuntime(), "SomeAssembly", " ", default, "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -45,7 +45,7 @@ namespace Microsoft.JSInterop.Infrastructure var assemblyName = "Some.Fake.Assembly"; var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null); + DotNetDispatcher.Invoke(new TestJSRuntime(), assemblyName, "SomeMethod", default, null); }); Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message); @@ -67,52 +67,56 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, default, null); + DotNetDispatcher.Invoke(new TestJSRuntime(), thisAssemblyName, methodIdentifier, default, null); }); Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message); } [Fact] - public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeStaticVoidMethod() { // Arrange/Act + var jsRuntime = new TestJSRuntime(); SomePublicType.DidInvokeMyInvocableStaticVoid = false; - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticVoid", default, null); // Assert Assert.Null(resultJson); Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid); - }); + } [Fact] - public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeStaticNonVoidMethod() { // Arrange/Act - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null); - var result = JsonSerializer.Deserialize(resultJson, JsonSerializerOptionsProvider.Options); + var jsRuntime = new TestJSRuntime(); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticNonVoid", default, null); + var result = JsonSerializer.Deserialize(resultJson, jsRuntime.JsonSerializerOptions); // Assert Assert.Equal("Test", result.StringVal); Assert.Equal(123, result.IntVal); - }); + } [Fact] - public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime => + public void CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() { // Arrange/Act - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); - var result = JsonSerializer.Deserialize(resultJson, JsonSerializerOptionsProvider.Options); + var jsRuntime = new TestJSRuntime(); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); + var result = JsonSerializer.Deserialize(resultJson, jsRuntime.JsonSerializerOptions); // Assert Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal); Assert.Equal(456, result.IntVal); - }); + } [Fact] - public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime => + public void CanInvokeStaticWithParams() { // Arrange: Track a .NET object to use as an arg + var jsRuntime = new TestJSRuntime(); var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; var objectRef = DotNetObjectReference.Create(arg3); jsRuntime.Invoke("unimportant", objectRef); @@ -123,15 +127,15 @@ namespace Microsoft.JSInterop.Infrastructure new TestDTO { StringVal = "Another string", IntVal = 456 }, new[] { 100, 200 }, objectRef - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); var result = JsonDocument.Parse(resultJson); var root = result.RootElement; // Assert: First result value marshalled via JSON - var resultDto1 = JsonSerializer.Deserialize(root[0].GetRawText(), JsonSerializerOptionsProvider.Options); + var resultDto1 = JsonSerializer.Deserialize(root[0].GetRawText(), jsRuntime.JsonSerializerOptions); Assert.Equal("ANOTHER STRING", resultDto1.StringVal); Assert.Equal(756, resultDto1.IntVal); @@ -142,15 +146,16 @@ namespace Microsoft.JSInterop.Infrastructure Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _)); Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property)); - var resultDto2 = Assert.IsType>(DotNetObjectReferenceManager.Current.FindDotNetObject(property.GetInt64())).Value; + var resultDto2 = Assert.IsType>(jsRuntime.GetObjectReference(property.GetInt64())).Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(1299, resultDto2.IntVal); - }); + } [Fact] - public Task InvokingWithIncorrectUseOfDotNetObjectRefThrows() => WithJSRuntime(jsRuntime => + public void InvokingWithIncorrectUseOfDotNetObjectRefThrows() { // Arrange + var jsRuntime = new TestJSRuntime(); var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage); var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; var objectRef = DotNetObjectReference.Create(arg3); @@ -162,67 +167,72 @@ namespace Microsoft.JSInterop.Infrastructure new TestDTO { StringVal = "Another string", IntVal = 456 }, new[] { 100, 200 }, objectRef - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act & Assert var ex = Assert.Throws(() => - DotNetDispatcher.Invoke(thisAssemblyName, method, default, argsJson)); + DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, method, default, argsJson)); Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 3 must be declared as type 'DotNetObjectRef' to receive the incoming value.", ex.Message); - }); + } [Fact] - public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeInstanceVoidMethod() { // Arrange: Track some instance + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null); // Assert Assert.Null(resultJson); Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid); - }); + } [Fact] - public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeBaseInstanceVoidMethod() { // Arrange: Track some instance + var jsRuntime = new TestJSRuntime(); var targetInstance = new DerivedClass(); var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "BaseClassInvokableInstanceVoid", 1, null); // Assert Assert.Null(resultJson); Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid); - }); + } [Fact] - public Task DotNetObjectReferencesCanBeDisposed() => WithJSRuntime(jsRuntime => + public void DotNetObjectReferencesCanBeDisposed() { // Arrange + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var objectRef = DotNetObjectReference.Create(targetInstance); + jsRuntime.Invoke("unimportant", objectRef); // Act - DotNetDispatcher.BeginInvokeDotNet(null, null, "__Dispose", objectRef.ObjectId, null); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, null, null, "__Dispose", objectRef.ObjectId, null); // Assert Assert.True(objectRef.Disposed); - }); + } [Fact] - public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime => + public void CannotUseDotNetObjectRefAfterDisposal() { // This test addresses the case where the developer calls objectRef.Dispose() // from .NET code, as opposed to .dispose() from JS code // Arrange: Track some instance, then dispose it + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); @@ -230,17 +240,18 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); + () => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); - }); + } [Fact] - public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(jsRuntime => + public void CannotUseDotNetObjectRefAfterReleaseDotNetObject() { // This test addresses the case where the developer calls .dispose() // from JS code, as opposed to objectRef.Dispose() from .NET code // Arrange: Track some instance, then dispose it + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); @@ -248,80 +259,85 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); + () => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); - }); + } [Fact] - public Task EndInvoke_WithSuccessValue() => WithJSRuntime(jsRuntime => + public void EndInvoke_WithSuccessValue() { // Arrange + var jsRuntime = new TestJSRuntime(); var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 }; var task = jsRuntime.InvokeAsync("unimportant"); - var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, JsonSerializerOptionsProvider.Options); + var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions); // Act - DotNetDispatcher.EndInvokeJS(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert Assert.True(task.IsCompletedSuccessfully); var result = task.Result; Assert.Equal(testDTO.StringVal, result.StringVal); Assert.Equal(testDTO.IntVal, result.IntVal); - }); + } [Fact] - public Task EndInvoke_WithErrorString() => WithJSRuntime(async jsRuntime => + public async Task EndInvoke_WithErrorString() { // Arrange + var jsRuntime = new TestJSRuntime(); var expected = "Some error"; var task = jsRuntime.InvokeAsync("unimportant"); - var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, JsonSerializerOptionsProvider.Options); + var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, jsRuntime.JsonSerializerOptions); // Act - DotNetDispatcher.EndInvokeJS(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert var ex = await Assert.ThrowsAsync(async () => await task); Assert.Equal(expected, ex.Message); - }); + } [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12357")] - public Task EndInvoke_AfterCancel() => WithJSRuntime(jsRuntime => + public void EndInvoke_AfterCancel() { // Arrange + var jsRuntime = new TestJSRuntime(); var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 }; var cts = new CancellationTokenSource(); var task = jsRuntime.InvokeAsync("unimportant", cts.Token); - var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, JsonSerializerOptionsProvider.Options); + var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions); // Act cts.Cancel(); - DotNetDispatcher.EndInvokeJS(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert Assert.True(task.IsCanceled); - }); + } [Fact] - public Task EndInvoke_WithNullError() => WithJSRuntime(async jsRuntime => + public async Task EndInvoke_WithNullError() { // Arrange + var jsRuntime = new TestJSRuntime(); var task = jsRuntime.InvokeAsync("unimportant"); - var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, JsonSerializerOptionsProvider.Options); + var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, jsRuntime.JsonSerializerOptions); // Act - DotNetDispatcher.EndInvokeJS(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert var ex = await Assert.ThrowsAsync(async () => await task); Assert.Empty(ex.Message); - }); + } [Fact] - public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime => + public void CanInvokeInstanceMethodWithParams() { // Arrange: Track some instance plus another object we'll pass as a param + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; jsRuntime.Invoke("unimportant", @@ -330,38 +346,40 @@ namespace Microsoft.JSInterop.Infrastructure var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]"; // Act - var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceMethod", 1, argsJson); // Assert Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson); - var resultDto = ((DotNetObjectReference)jsRuntime.ObjectRefManager.FindDotNetObject(3)).Value; + var resultDto = ((DotNetObjectReference)jsRuntime.GetObjectReference(3)).Value; Assert.Equal(1235, resultDto.IntVal); Assert.Equal("MY STRING", resultDto.StringVal); - }); + } [Fact] - public Task CannotInvokeWithFewerNumberOfParameters() => WithJSRuntime(jsRuntime => + public void CannotInvokeWithFewerNumberOfParameters() { // Arrange + var jsRuntime = new TestJSRuntime(); var argsJson = JsonSerializer.Serialize(new object[] { new TestDTO { StringVal = "Another string", IntVal = 456 }, new[] { 100, 200 }, - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); }); Assert.Equal("The call to 'InvocableStaticWithParams' expects '3' parameters, but received '2'.", ex.Message); - }); + } [Fact] - public Task CannotInvokeWithMoreParameters() => WithJSRuntime(jsRuntime => + public void CannotInvokeWithMoreParameters() { // Arrange + var jsRuntime = new TestJSRuntime(); var objectRef = DotNetObjectReference.Create(new TestDTO { IntVal = 4 }); var argsJson = JsonSerializer.Serialize(new object[] { @@ -369,21 +387,22 @@ namespace Microsoft.JSInterop.Infrastructure new[] { 100, 200 }, objectRef, 7, - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); }); Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message); - }); + } [Fact] - public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime => + public async Task CanInvokeAsyncMethod() { // Arrange: Track some instance plus another object we'll pass as a param + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; var arg1Ref = DotNetObjectReference.Create(targetInstance); @@ -395,12 +414,12 @@ namespace Microsoft.JSInterop.Infrastructure { new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, arg2Ref, - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, null, "InvokableAsyncMethod", 1, argsJson); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableAsyncMethod", 1, argsJson); await resultTask; // Assert: Correct completion information @@ -417,17 +436,18 @@ namespace Microsoft.JSInterop.Infrastructure var resultDto2 = resultDto2Ref.Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(2468, resultDto2.IntVal); - }); + } [Fact] - public Task CanInvokeSyncThrowingMethod() => WithJSRuntime(async jsRuntime => + public async Task CanInvokeSyncThrowingMethod() { // Arrange + var jsRuntime = new TestJSRuntime(); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -439,17 +459,18 @@ namespace Microsoft.JSInterop.Infrastructure // https://github.com/aspnet/AspNetCore/issues/8612 var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception); - }); + } [Fact] - public Task CanInvokeAsyncThrowingMethod() => WithJSRuntime(async jsRuntime => + public async Task CanInvokeAsyncThrowingMethod() { // Arrange + var jsRuntime = new TestJSRuntime(); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -461,15 +482,16 @@ namespace Microsoft.JSInterop.Infrastructure // https://github.com/aspnet/AspNetCore/issues/8612 var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); - }); + } [Fact] - public Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId() => WithJSRuntime(async jsRuntime => + public async Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId() { // Arrange + var jsRuntime = new TestJSRuntime(); var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, "InvocableStaticWithParams", default, "not json"); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, "InvocableStaticWithParams", default, "not json"); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -478,29 +500,30 @@ namespace Microsoft.JSInterop.Infrastructure Assert.False(jsRuntime.LastCompletionStatus); // Fails var result = Assert.IsType(jsRuntime.LastCompletionResult); Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result.SourceException.ToString()); - }); + } [Fact] - public Task BeginInvoke_ThrowsWithInvalid_DotNetObjectRef() => WithJSRuntime(jsRuntime => + public void BeginInvoke_ThrowsWithInvalid_DotNetObjectRef() { // Arrange + var jsRuntime = new TestJSRuntime(); var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, null, "InvokableInstanceVoid", 1, null); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableInstanceVoid", 1, null); // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); Assert.False(jsRuntime.LastCompletionStatus); // Fails var result = Assert.IsType(jsRuntime.LastCompletionResult); - Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectRef instance was already disposed.", result.SourceException.ToString()); - }); + Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectReference instance was already disposed.", result.SourceException.ToString()); + } [Theory] [InlineData("")] [InlineData("")] public void ParseArguments_ThrowsIfJsonIsInvalid(string arguments) { - Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) })); + Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) })); } [Theory] @@ -509,7 +532,7 @@ namespace Microsoft.JSInterop.Infrastructure public void ParseArguments_ThrowsIfTheArgsJsonIsNotArray(string arguments) { // Act & Assert - Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) })); + Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) })); } [Theory] @@ -518,7 +541,7 @@ namespace Microsoft.JSInterop.Infrastructure public void ParseArguments_ThrowsIfTheArgsJsonIsInvalidArray(string arguments) { // Act & Assert - Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) })); + Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) })); } [Fact] @@ -528,7 +551,7 @@ namespace Microsoft.JSInterop.Infrastructure var arguments = "[\"Hello\", 2]"; // Act - var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string), typeof(int), }); + var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string), typeof(int), }); // Assert Assert.Equal(new object[] { "Hello", 2 }, result); @@ -541,7 +564,7 @@ namespace Microsoft.JSInterop.Infrastructure var arguments = "[{\"IntVal\": 7}]"; // Act - var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(TestDTO), }); + var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(TestDTO), }); // Assert var value = Assert.IsType(Assert.Single(result)); @@ -556,7 +579,7 @@ namespace Microsoft.JSInterop.Infrastructure var arguments = "[4, null]"; // Act - var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(int), typeof(TestDTO), }); + var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(int), typeof(TestDTO), }); // Assert Assert.Collection( @@ -573,92 +596,72 @@ namespace Microsoft.JSInterop.Infrastructure var arguments = "[4, {\"__dotNetObject\": 7}]"; // Act - var ex = Assert.Throws(() => DotNetDispatcher.ParseArguments(method, arguments, new[] { typeof(int), typeof(TestDTO), })); + var ex = Assert.Throws(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), method, arguments, new[] { typeof(int), typeof(TestDTO), })); // Assert Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 2 must be declared as type 'DotNetObjectRef' to receive the incoming value.", ex.Message); } [Fact] - public void ParseEndInvokeArguments_ThrowsIfJsonIsEmptyString() + public void EndInvokeJS_ThrowsIfJsonIsEmptyString() { - Assert.ThrowsAny(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "")); + Assert.ThrowsAny(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "")); } [Fact] - public void ParseEndInvokeArguments_ThrowsIfJsonIsNotArray() + public void EndInvokeJS_ThrowsIfJsonIsNotArray() { - Assert.ThrowsAny(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "{\"key\": \"value\"}")); + Assert.ThrowsAny(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "{\"key\": \"value\"}")); } [Fact] - public void ParseEndInvokeArguments_ThrowsIfJsonArrayIsInComplete() + public void EndInvokeJS_ThrowsIfJsonArrayIsInComplete() { - Assert.ThrowsAny(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "[7, false")); + Assert.ThrowsAny(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false")); } [Fact] - public void ParseEndInvokeArguments_ThrowsIfJsonArrayHasMoreThan3Arguments() + public void EndInvokeJS_ThrowsIfJsonArrayHasMoreThan3Arguments() { - Assert.ThrowsAny(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "[7, false, \"Hello\", 5]")); + Assert.ThrowsAny(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false, \"Hello\", 5]")); } [Fact] - public void ParseEndInvokeArguments_Works() + public void EndInvokeJS_Works() { var jsRuntime = new TestJSRuntime(); var task = jsRuntime.InvokeAsync("somemethod"); - DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]"); + DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]"); Assert.True(task.IsCompletedSuccessfully); Assert.Equal(7, task.Result.IntVal); } [Fact] - public void ParseEndInvokeArguments_WithArrayValue() + public void EndInvokeJS_WithArrayValue() { var jsRuntime = new TestJSRuntime(); var task = jsRuntime.InvokeAsync("somemethod"); - DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]"); + DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]"); Assert.True(task.IsCompletedSuccessfully); Assert.Equal(new[] { 1, 2, 3 }, task.Result); } [Fact] - public void ParseEndInvokeArguments_WithNullValue() + public void EndInvokeJS_WithNullValue() { var jsRuntime = new TestJSRuntime(); var task = jsRuntime.InvokeAsync("somemethod"); - DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]"); + DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]"); Assert.True(task.IsCompletedSuccessfully); Assert.Null(task.Result); } - Task WithJSRuntime(Action testCode) - { - return WithJSRuntime(jsRuntime => - { - testCode(jsRuntime); - return Task.CompletedTask; - }); - } - - async Task WithJSRuntime(Func testCode) - { - // Since the tests rely on the asynclocal JSRuntime.Current, ensure we - // are on a distinct async context with a non-null JSRuntime.Current - await Task.Yield(); - - var runtime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(runtime); - await testCode(runtime); - } - internal class SomeInteralType { [JSInvokable("MethodOnInternalType")] public void MyMethod() { } diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs index 541ad2b025..8d055aea2c 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs @@ -2,91 +2,93 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Text.Json; -using System.Threading.Tasks; using Xunit; -using static Microsoft.JSInterop.TestJSRuntime; namespace Microsoft.JSInterop.Infrastructure { public class DotNetObjectReferenceJsonConverterTest { + private readonly JSRuntime JSRuntime = new TestJSRuntime(); + private JsonSerializerOptions JsonSerializerOptions => JSRuntime.JsonSerializerOptions; + [Fact] - public Task Read_Throws_IfJsonIsMissingDotNetObjectProperty() => WithJSRuntime(_ => + public void Read_Throws_IfJsonIsMissingDotNetObjectProperty() { // Arrange + var jsRuntime = new TestJSRuntime(); var dotNetObjectRef = DotNetObjectReference.Create(new TestModel()); var json = "{}"; // Act & Assert - var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json)); + var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, JsonSerializerOptions)); Assert.Equal("Required property __dotNetObject not found.", ex.Message); - }); + } [Fact] - public Task Read_Throws_IfJsonContainsUnknownContent() => WithJSRuntime(_ => + public void Read_Throws_IfJsonContainsUnknownContent() { // Arrange + var jsRuntime = new TestJSRuntime(); var dotNetObjectRef = DotNetObjectReference.Create(new TestModel()); var json = "{\"foo\":2}"; // Act & Assert - var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json)); + var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, JsonSerializerOptions)); Assert.Equal("Unexcepted JSON property foo.", ex.Message); - }); + } [Fact] - public Task Read_Throws_IfJsonIsIncomplete() => WithJSRuntime(_ => + public void Read_Throws_IfJsonIsIncomplete() { // Arrange var input = new TestModel(); var dotNetObjectRef = DotNetObjectReference.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = $"{{\"__dotNetObject\":{objectId}"; // Act & Assert - var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); + var ex = Record.Exception(() => JsonSerializer.Deserialize>(json, JsonSerializerOptions)); Assert.IsAssignableFrom(ex); - }); + } [Fact] - public Task Read_Throws_IfDotNetObjectIdAppearsMultipleTimes() => WithJSRuntime(_ => + public void Read_Throws_IfDotNetObjectIdAppearsMultipleTimes() { // Arrange var input = new TestModel(); var dotNetObjectRef = DotNetObjectReference.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = $"{{\"__dotNetObject\":{objectId},\"__dotNetObject\":{objectId}}}"; // Act & Assert - var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); + var ex = Record.Exception(() => JsonSerializer.Deserialize>(json, JsonSerializerOptions)); Assert.IsAssignableFrom(ex); - }); + } [Fact] - public Task Read_ReadsJson() => WithJSRuntime(_ => + public void Read_ReadsJson() { // Arrange var input = new TestModel(); var dotNetObjectRef = DotNetObjectReference.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = $"{{\"__dotNetObject\":{objectId}}}"; // Act - var deserialized = JsonSerializer.Deserialize>(json); + var deserialized = JsonSerializer.Deserialize>(json, JsonSerializerOptions); // Assert Assert.Same(input, deserialized.Value); Assert.Equal(objectId, deserialized.ObjectId); - }); - + } [Fact] - public Task Read_ReturnsTheCorrectInstance() => WithJSRuntime(_ => + public void Read_ReturnsTheCorrectInstance() { // Arrange // Track a few instances and verify that the deserialized value returns the correct value. @@ -95,23 +97,23 @@ namespace Microsoft.JSInterop.Infrastructure var ref1 = DotNetObjectReference.Create(instance1); var ref2 = DotNetObjectReference.Create(instance2); - var json = $"[{{\"__dotNetObject\":{ref2.ObjectId}}},{{\"__dotNetObject\":{ref1.ObjectId}}}]"; + var json = $"[{{\"__dotNetObject\":{JSRuntime.TrackObjectReference(ref1)}}},{{\"__dotNetObject\":{JSRuntime.TrackObjectReference(ref2)}}}]"; // Act - var deserialized = JsonSerializer.Deserialize[]>(json); + var deserialized = JsonSerializer.Deserialize[]>(json, JsonSerializerOptions); // Assert - Assert.Same(instance2, deserialized[0].Value); - Assert.Same(instance1, deserialized[1].Value); - }); + Assert.Same(instance1, deserialized[0].Value); + Assert.Same(instance2, deserialized[1].Value); + } [Fact] - public Task Read_ReadsJson_WithFormatting() => WithJSRuntime(_ => + public void Read_ReadsJson_WithFormatting() { // Arrange var input = new TestModel(); var dotNetObjectRef = DotNetObjectReference.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = @$"{{ @@ -119,27 +121,27 @@ namespace Microsoft.JSInterop.Infrastructure }}"; // Act - var deserialized = JsonSerializer.Deserialize>(json); + var deserialized = JsonSerializer.Deserialize>(json, JsonSerializerOptions); // Assert Assert.Same(input, deserialized.Value); Assert.Equal(objectId, deserialized.ObjectId); - }); + } [Fact] - public Task WriteJsonTwice_KeepsObjectId() => WithJSRuntime(_ => + public void WriteJsonTwice_KeepsObjectId() { // Arrange var dotNetObjectRef = DotNetObjectReference.Create(new TestModel()); // Act - var json1 = JsonSerializer.Serialize(dotNetObjectRef); - var json2 = JsonSerializer.Serialize(dotNetObjectRef); + var json1 = JsonSerializer.Serialize(dotNetObjectRef, JsonSerializerOptions); + var json2 = JsonSerializer.Serialize(dotNetObjectRef, JsonSerializerOptions); // Assert Assert.Equal($"{{\"__dotNetObject\":{dotNetObjectRef.ObjectId}}}", json1); Assert.Equal(json1, json2); - }); + } private class TestModel { diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs index 4054101258..a1caff595b 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs @@ -18,7 +18,6 @@ namespace Microsoft.JSInterop { NextResultJson = "{\"intValue\":123,\"stringValue\":\"Hello\"}" }; - JSRuntime.SetCurrentJSRuntime(runtime); // Act var syncResult = runtime.Invoke("test identifier 1", "arg1", 123, true); @@ -36,7 +35,6 @@ namespace Microsoft.JSInterop { // Arrange var runtime = new TestJSInProcessRuntime { NextResultJson = null }; - JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); var obj3 = new object(); @@ -60,9 +58,9 @@ namespace Microsoft.JSInterop Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3}}]", call.ArgsJson); // Assert: Objects were tracked - Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1).Value); - Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2).Value); - Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3).Value); + Assert.Same(obj1, runtime.GetObjectReference(1).Value); + Assert.Same(obj2, runtime.GetObjectReference(2).Value); + Assert.Same(obj3, runtime.GetObjectReference(3).Value); } [Fact] @@ -73,7 +71,6 @@ namespace Microsoft.JSInterop { NextResultJson = "[{\"__dotNetObject\":2},{\"__dotNetObject\":1}]" }; - JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index 4e65ddeb0f..b102ecc0b5 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -14,23 +14,6 @@ namespace Microsoft.JSInterop { public class JSRuntimeTest { - #region this will be removed eventually - [Fact] - public async Task CanHaveDistinctJSRuntimeInstancesInEachAsyncContext() - { - var tasks = Enumerable.Range(0, 20).Select(async _ => - { - var jsRuntime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(jsRuntime); - await Task.Delay(50).ConfigureAwait(false); - Assert.Same(jsRuntime, JSRuntime.Current); - }); - - await Task.WhenAll(tasks); - Assert.Null(JSRuntime.Current); - } - #endregion - [Fact] public void DispatchesAsyncCallsWithDistinctAsyncHandles() { @@ -274,7 +257,6 @@ namespace Microsoft.JSInterop { // Arrange var runtime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); var obj3 = new object(); @@ -296,15 +278,15 @@ namespace Microsoft.JSInterop // Assert: Serialized as expected var call = runtime.BeginInvokeCalls.Single(); Assert.Equal("test identifier", call.Identifier); - Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":3},\"obj3\":{\"__dotNetObject\":4},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":2}}]", call.ArgsJson); + Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":4}}]", call.ArgsJson); // Assert: Objects were tracked - Assert.Same(obj1Ref, runtime.ObjectRefManager.FindDotNetObject(1)); + Assert.Same(obj1Ref, runtime.GetObjectReference(1)); Assert.Same(obj1, obj1Ref.Value); - Assert.NotSame(obj1Ref, runtime.ObjectRefManager.FindDotNetObject(2)); - Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2).Value); - Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3).Value); - Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4).Value); + Assert.NotSame(obj1Ref, runtime.GetObjectReference(2)); + Assert.Same(obj2, runtime.GetObjectReference(2).Value); + Assert.Same(obj3, runtime.GetObjectReference(3).Value); + Assert.Same(obj1, runtime.GetObjectReference(4).Value); } [Fact] diff --git a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs index 48782fc4df..740f02b8da 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Threading.Tasks; namespace Microsoft.JSInterop { @@ -17,16 +16,5 @@ namespace Microsoft.JSInterop { throw new NotImplementedException(); } - - public static async Task WithJSRuntime(Action testCode) - { - // Since the tests rely on the asynclocal JSRuntime.Current, ensure we - // are on a distinct async context with a non-null JSRuntime.Current - await Task.Yield(); - - var runtime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(runtime); - testCode(runtime); - } } } diff --git a/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs index 2e4defd1b7..0996795ca3 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs @@ -8,6 +8,7 @@ namespace Mono.WebAssembly.Interop public MonoWebAssemblyJSRuntime() { } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { } protected override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) { } + protected static void Initialize(Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime jsRuntime) { } protected override string InvokeJS(string identifier, string argsJson) { throw null; } public TRes InvokeUnmarshalled(string identifier) { throw null; } public TRes InvokeUnmarshalled(string identifier, T0 arg0) { throw null; } diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs index 0e292a3e3c..b6b01d754d 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs @@ -16,6 +16,25 @@ namespace Mono.WebAssembly.Interop /// public class MonoWebAssemblyJSRuntime : JSInProcessRuntime { + /// + /// Gets the used to perform operations using . + /// + private static MonoWebAssemblyJSRuntime Instance { get; set; } + + /// + /// Initializes the to be used to perform operations using . + /// + /// The instance. + protected static void Initialize(MonoWebAssemblyJSRuntime jsRuntime) + { + if (Instance != null) + { + throw new InvalidOperationException("MonoWebAssemblyJSRuntime has already been initialized."); + } + + Instance = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); + } + /// protected override string InvokeJS(string identifier, string argsJson) { @@ -34,11 +53,11 @@ namespace Mono.WebAssembly.Interop // Invoked via Mono's JS interop mechanism (invoke_method) private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson) - => DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); + => DotNetDispatcher.Invoke(Instance, assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); // Invoked via Mono's JS interop mechanism (invoke_method) private static void EndInvokeJS(string argsJson) - => DotNetDispatcher.EndInvokeJS(argsJson); + => DotNetDispatcher.EndInvokeJS(Instance, argsJson); // Invoked via Mono's JS interop mechanism (invoke_method) private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) @@ -59,7 +78,7 @@ namespace Mono.WebAssembly.Interop assemblyName = assemblyNameOrDotNetObjectId; } - DotNetDispatcher.BeginInvokeDotNet(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); + DotNetDispatcher.BeginInvokeDotNet(Instance, callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); } protected override void EndInvokeDotNet( @@ -84,7 +103,7 @@ namespace Mono.WebAssembly.Interop // We pass 0 as the async handle because we don't want the JS-side code to // send back any notification (we're just providing a result for an existing async call) - var args = JsonSerializer.Serialize(new[] { callId, success, resultOrError }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var args = JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions); BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); }