From 9372816b7c436e06da6d6426e0863b3677aac83c Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 14 Aug 2019 12:24:35 -0700 Subject: [PATCH] API Review: JSRuntime (dotnet/extensions#2166) * API Review: JSRuntime * Rename JSRuntimeBase -> JSRuntime * Rename JSInProcessRuntimeBase -> JSInProcessRuntime * Rename DotNetObjectRef -> DotNetObjectReference * Update JSRuntime to return ValueTask * Make InvokeAsync APIs that explicitly cancels and API that default cancels more crisp * Introduce void invoking APIs * Fixup method names on DotNetDispatcher \n\nCommit migrated from https://github.com/dotnet/extensions/commit/93d3ae448551cac29af8cf882b31047f5da0dadc --- .../ref/Microsoft.JSInterop.netcoreapp3.0.cs | 63 +-- .../ref/Microsoft.JSInterop.netstandard2.0.cs | 63 +-- ...tObjectRef.cs => DotNetObjectReference.cs} | 14 +- ...tRefOfT.cs => DotNetObjectReferenceOfT.cs} | 18 +- .../Microsoft.JSInterop/src/IJSRuntime.cs | 14 +- .../{ => Infrastructure}/DotNetDispatcher.cs | 28 +- .../DotNetObjectReferenceJsonConverter.cs | 12 +- ...NetObjectReferenceJsonConverterFactory.cs} | 4 +- .../DotNetObjectReferenceManager.cs} | 20 +- .../IDotNetObjectReference.cs} | 4 +- .../{ => Infrastructure}/TaskGenericsUtil.cs | 2 +- ...ssRuntimeBase.cs => JSInProcessRuntime.cs} | 2 +- .../src/JSInProcessRuntimeExtensions.cs | 29 ++ .../src/JSInvokableAttribute.cs | 2 +- .../Microsoft.JSInterop/src/JSRuntime.cs | 169 +++++++- .../Microsoft.JSInterop/src/JSRuntimeBase.cs | 174 -------- .../src/JSRuntimeExtensions.cs | 140 +++++++ ...efTest.cs => DotNetObjectReferenceTest.cs} | 6 +- .../DotNetDispatcherTest.cs | 78 ++-- .../DotNetObjectReferenceJsonConverterTest.cs | 34 +- .../test/JSInProcessRuntimeExtensionsTest.cs | 27 ++ ...eBaseTest.cs => JSInProcessRuntimeTest.cs} | 18 +- .../test/JSRuntimeBaseTest.cs | 386 ------------------ .../test/JSRuntimeExtensionsTest.cs | 181 ++++++++ .../Microsoft.JSInterop/test/JSRuntimeTest.cs | 380 ++++++++++++++++- .../Microsoft.JSInterop/test/TestJSRuntime.cs | 4 +- ...Mono.WebAssembly.Interop.netstandard2.0.cs | 2 +- .../src/MonoWebAssemblyJSRuntime.cs | 7 +- 28 files changed, 1133 insertions(+), 748 deletions(-) rename src/JSInterop/Microsoft.JSInterop/src/{DotNetObjectRef.cs => DotNetObjectReference.cs} (54%) rename src/JSInterop/Microsoft.JSInterop/src/{DotNetObjectRefOfT.cs => DotNetObjectReferenceOfT.cs} (80%) rename src/JSInterop/Microsoft.JSInterop/src/{ => Infrastructure}/DotNetDispatcher.cs (94%) rename src/JSInterop/Microsoft.JSInterop/src/{ => Infrastructure}/DotNetObjectReferenceJsonConverter.cs (77%) rename src/JSInterop/Microsoft.JSInterop/src/{DotNetObjectRefJsonConverterFactory.cs => Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs} (90%) rename src/JSInterop/Microsoft.JSInterop/src/{DotNetObjectRefManager.cs => Infrastructure/DotNetObjectReferenceManager.cs} (74%) rename src/JSInterop/Microsoft.JSInterop/src/{IDotNetObjectRef.cs => Infrastructure/IDotNetObjectReference.cs} (68%) rename src/JSInterop/Microsoft.JSInterop/src/{ => Infrastructure}/TaskGenericsUtil.cs (98%) rename src/JSInterop/Microsoft.JSInterop/src/{JSInProcessRuntimeBase.cs => JSInProcessRuntime.cs} (95%) create mode 100644 src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs delete mode 100644 src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs create mode 100644 src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs rename src/JSInterop/Microsoft.JSInterop/test/{DotNetObjectRefTest.cs => DotNetObjectReferenceTest.cs} (83%) rename src/JSInterop/Microsoft.JSInterop/test/{ => Infrastructure}/DotNetDispatcherTest.cs (91%) rename src/JSInterop/Microsoft.JSInterop/test/{ => Infrastructure}/DotNetObjectReferenceJsonConverterTest.cs (80%) create mode 100644 src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeExtensionsTest.cs rename src/JSInterop/Microsoft.JSInterop/test/{JSInProcessRuntimeBaseTest.cs => JSInProcessRuntimeTest.cs} (88%) delete mode 100644 src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs create mode 100644 src/JSInterop/Microsoft.JSInterop/test/JSRuntimeExtensionsTest.cs 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 e73fa1be69..a5fbbc768a 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs @@ -3,19 +3,13 @@ namespace Microsoft.JSInterop { - public static partial class DotNetDispatcher + public static partial class DotNetObjectReference { - 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; } + public static Microsoft.JSInterop.DotNetObjectReference Create(TValue value) where TValue : class { throw null; } } - public static partial class DotNetObjectRef + public sealed partial class DotNetObjectReference : System.IDisposable where TValue : class { - public static Microsoft.JSInterop.DotNetObjectRef Create(TValue value) where TValue : class { throw null; } - } - public sealed partial class DotNetObjectRef : System.IDisposable where TValue : class - { - internal DotNetObjectRef() { } + internal DotNetObjectReference() { } public TValue Value { get { throw null; } } public void Dispose() { } } @@ -25,38 +19,61 @@ namespace Microsoft.JSInterop } public partial interface IJSRuntime { - System.Threading.Tasks.Task InvokeAsync(string identifier, System.Collections.Generic.IEnumerable args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); - System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args); + System.Threading.Tasks.ValueTask InvokeAsync(string identifier, object[] args); + System.Threading.Tasks.ValueTask InvokeAsync(string identifier, System.Threading.CancellationToken cancellationToken, object[] args); } public partial class JSException : System.Exception { public JSException(string message) { } public JSException(string message, System.Exception innerException) { } } - public abstract partial class JSInProcessRuntimeBase : Microsoft.JSInterop.JSRuntimeBase, Microsoft.JSInterop.IJSInProcessRuntime, Microsoft.JSInterop.IJSRuntime + public abstract partial class JSInProcessRuntime : Microsoft.JSInterop.JSRuntime, Microsoft.JSInterop.IJSInProcessRuntime, Microsoft.JSInterop.IJSRuntime { - protected JSInProcessRuntimeBase() { } + protected JSInProcessRuntime() { } protected abstract string InvokeJS(string identifier, string argsJson); public TValue Invoke(string identifier, params object[] args) { throw null; } } + public static partial class JSInProcessRuntimeExtensions + { + public static void InvokeVoid(this Microsoft.JSInterop.IJSInProcessRuntime jsRuntime, string identifier, params object[] args) { } + } [System.AttributeUsageAttribute(System.AttributeTargets.Method, AllowMultiple=true)] - public partial class JSInvokableAttribute : System.Attribute + public sealed partial class JSInvokableAttribute : System.Attribute { public JSInvokableAttribute() { } public JSInvokableAttribute(string identifier) { } public string Identifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } - public static partial class JSRuntime + public abstract partial class JSRuntime : Microsoft.JSInterop.IJSRuntime { - public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } - } - public abstract partial class JSRuntimeBase : Microsoft.JSInterop.IJSRuntime - { - protected JSRuntimeBase() { } + protected JSRuntime() { } protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } 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.Task InvokeAsync(string identifier, System.Collections.Generic.IEnumerable args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args) { throw null; } + 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 + { + public static System.Threading.Tasks.ValueTask InvokeAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, params object[] args) { throw null; } + public static System.Threading.Tasks.ValueTask InvokeAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, System.Threading.CancellationToken cancellationToken, params object[] args) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.ValueTask InvokeAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, System.TimeSpan timeout, params object[] args) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.ValueTask InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, params object[] args) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.ValueTask InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, System.Threading.CancellationToken cancellationToken, params object[] args) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.ValueTask InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, System.TimeSpan timeout, params object[] args) { throw null; } + } +} +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; } } } 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 e73fa1be69..a5fbbc768a 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -3,19 +3,13 @@ namespace Microsoft.JSInterop { - public static partial class DotNetDispatcher + public static partial class DotNetObjectReference { - 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; } + public static Microsoft.JSInterop.DotNetObjectReference Create(TValue value) where TValue : class { throw null; } } - public static partial class DotNetObjectRef + public sealed partial class DotNetObjectReference : System.IDisposable where TValue : class { - public static Microsoft.JSInterop.DotNetObjectRef Create(TValue value) where TValue : class { throw null; } - } - public sealed partial class DotNetObjectRef : System.IDisposable where TValue : class - { - internal DotNetObjectRef() { } + internal DotNetObjectReference() { } public TValue Value { get { throw null; } } public void Dispose() { } } @@ -25,38 +19,61 @@ namespace Microsoft.JSInterop } public partial interface IJSRuntime { - System.Threading.Tasks.Task InvokeAsync(string identifier, System.Collections.Generic.IEnumerable args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); - System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args); + System.Threading.Tasks.ValueTask InvokeAsync(string identifier, object[] args); + System.Threading.Tasks.ValueTask InvokeAsync(string identifier, System.Threading.CancellationToken cancellationToken, object[] args); } public partial class JSException : System.Exception { public JSException(string message) { } public JSException(string message, System.Exception innerException) { } } - public abstract partial class JSInProcessRuntimeBase : Microsoft.JSInterop.JSRuntimeBase, Microsoft.JSInterop.IJSInProcessRuntime, Microsoft.JSInterop.IJSRuntime + public abstract partial class JSInProcessRuntime : Microsoft.JSInterop.JSRuntime, Microsoft.JSInterop.IJSInProcessRuntime, Microsoft.JSInterop.IJSRuntime { - protected JSInProcessRuntimeBase() { } + protected JSInProcessRuntime() { } protected abstract string InvokeJS(string identifier, string argsJson); public TValue Invoke(string identifier, params object[] args) { throw null; } } + public static partial class JSInProcessRuntimeExtensions + { + public static void InvokeVoid(this Microsoft.JSInterop.IJSInProcessRuntime jsRuntime, string identifier, params object[] args) { } + } [System.AttributeUsageAttribute(System.AttributeTargets.Method, AllowMultiple=true)] - public partial class JSInvokableAttribute : System.Attribute + public sealed partial class JSInvokableAttribute : System.Attribute { public JSInvokableAttribute() { } public JSInvokableAttribute(string identifier) { } public string Identifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } - public static partial class JSRuntime + public abstract partial class JSRuntime : Microsoft.JSInterop.IJSRuntime { - public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } - } - public abstract partial class JSRuntimeBase : Microsoft.JSInterop.IJSRuntime - { - protected JSRuntimeBase() { } + protected JSRuntime() { } protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } 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.Task InvokeAsync(string identifier, System.Collections.Generic.IEnumerable args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } - public System.Threading.Tasks.Task InvokeAsync(string identifier, params object[] args) { throw null; } + 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 + { + public static System.Threading.Tasks.ValueTask InvokeAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, params object[] args) { throw null; } + public static System.Threading.Tasks.ValueTask InvokeAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, System.Threading.CancellationToken cancellationToken, params object[] args) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.ValueTask InvokeAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, System.TimeSpan timeout, params object[] args) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.ValueTask InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, params object[] args) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.ValueTask InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, System.Threading.CancellationToken cancellationToken, params object[] args) { throw null; } + [System.Diagnostics.DebuggerStepThroughAttribute] + public static System.Threading.Tasks.ValueTask InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime jsRuntime, string identifier, System.TimeSpan timeout, params object[] args) { throw null; } + } +} +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; } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs similarity index 54% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs rename to src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs index f604bab272..24b13f0c85 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs @@ -1,21 +1,23 @@ // 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 { /// - /// Provides convenience methods to produce a . + /// Provides convenience methods to produce a . /// - public static class DotNetObjectRef + public static class DotNetObjectReference { /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// The reference type to track. - /// An instance of . - public static DotNetObjectRef Create(TValue value) where TValue : class + /// An instance of . + public static DotNetObjectReference Create(TValue value) where TValue : class { - return new DotNetObjectRef(DotNetObjectRefManager.Current, value); + return new DotNetObjectReference(DotNetObjectReferenceManager.Current, value); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs similarity index 80% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs rename to src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs index d83d0e89bb..eb1ac6a234 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs @@ -3,6 +3,7 @@ using System; using System.Text.Json.Serialization; +using Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop { @@ -14,31 +15,24 @@ namespace Microsoft.JSInterop /// /// The type of the value to wrap. [JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))] - public sealed class DotNetObjectRef : IDotNetObjectRef, IDisposable where TValue : class + public sealed class DotNetObjectReference : IDotNetObjectReference, IDisposable where TValue : class { - private readonly DotNetObjectRefManager _referenceManager; + private readonly DotNetObjectReferenceManager _referenceManager; private readonly TValue _value; private readonly long _objectId; /// - /// Initializes a new instance of . + /// Initializes a new instance of . /// /// /// The value to pass by reference. - internal DotNetObjectRef(DotNetObjectRefManager referenceManager, TValue value) + internal DotNetObjectReference(DotNetObjectReferenceManager referenceManager, TValue value) { _referenceManager = referenceManager; _objectId = _referenceManager.TrackObject(this); _value = value; } - internal DotNetObjectRef(DotNetObjectRefManager referenceManager, long objectId, TValue value) - { - _referenceManager = referenceManager; - _objectId = objectId; - _value = value; - } - /// /// Gets the object instance represented by this wrapper. /// @@ -60,7 +54,7 @@ namespace Microsoft.JSInterop } } - object IDotNetObjectRef.Value => Value; + object IDotNetObjectReference.Value => Value; internal bool Disposed { get; private set; } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index d7ee372a73..6edc725177 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -1,7 +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 System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -14,21 +13,28 @@ namespace Microsoft.JSInterop { /// /// Invokes the specified JavaScript function asynchronously. + /// + /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, + /// consider using . + /// /// /// The JSON-serializable return type. /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. - Task InvokeAsync(string identifier, params object[] args); + ValueTask InvokeAsync(string identifier, object[] args); /// /// Invokes the specified JavaScript function asynchronously. /// /// The JSON-serializable return type. /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// /// JSON-serializable arguments. - /// A cancellation token to signal the cancellation of the operation. /// An instance of obtained by JSON-deserializing the return value. - Task InvokeAsync(string identifier, IEnumerable args, CancellationToken cancellationToken = default); + ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs similarity index 94% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index e639a33ff2..d4a4de14dd 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -11,7 +11,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; -namespace Microsoft.JSInterop +namespace Microsoft.JSInterop.Infrastructure { /// /// Provides methods that receive incoming calls from JS to .NET. @@ -39,10 +39,10 @@ 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. - IDotNetObjectRef targetInstance = default; + IDotNetObjectReference targetInstance = default; if (dotNetObjectId != default) { - targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); + targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId); } var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); @@ -63,7 +63,7 @@ namespace Microsoft.JSInterop /// 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 BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static void BeginInvokeDotNet(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 @@ -73,19 +73,19 @@ namespace Microsoft.JSInterop // 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 = (JSRuntimeBase)JSRuntime.Current; + var jsRuntimeBaseInstance = (JSRuntime)JSRuntime.Current; // Using ExceptionDispatchInfo here throughout because we want to always preserve // original stack traces. object syncResult = null; ExceptionDispatchInfo syncException = null; - IDotNetObjectRef targetInstance = null; + IDotNetObjectReference targetInstance = null; try { if (dotNetObjectId != default) { - targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); + targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId); } syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); @@ -128,7 +128,7 @@ namespace Microsoft.JSInterop } } - private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectRef objectReference, string argsJson) + private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson) { AssemblyKey assemblyKey; if (objectReference is null) @@ -227,7 +227,7 @@ namespace Microsoft.JSInterop jsonReader.ValueTextEquals(DotNetObjectRefKey.EncodedUtf8Bytes)) { // The JSON payload has the shape we expect from a DotNetObjectRef instance. - return !parameterType.IsGenericType || parameterType.GetGenericTypeDefinition() != typeof(DotNetObjectRef<>); + return !parameterType.IsGenericType || parameterType.GetGenericTypeDefinition() != typeof(DotNetObjectReference<>); } return false; @@ -239,9 +239,9 @@ namespace Microsoft.JSInterop /// associated as completed. /// /// - /// All exceptions from are caught + /// All exceptions from are caught /// are delivered via JS interop to the JavaScript side when it requests confirmation, as - /// the mechanism to call relies on + /// the mechanism to call relies on /// using JS->.NET interop. This overload is meant for directly triggering completion callbacks /// for .NET -> JS operations without going through JS interop, so the callsite for this /// method is responsible for handling any possible exception generated from the arguments @@ -252,13 +252,13 @@ namespace Microsoft.JSInterop /// 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 EndInvoke(string arguments) + public static void EndInvokeJS(string arguments) { - var jsRuntimeBase = (JSRuntimeBase)JSRuntime.Current; + var jsRuntimeBase = (JSRuntime)JSRuntime.Current; ParseEndInvokeArguments(jsRuntimeBase, arguments); } - internal static void ParseEndInvokeArguments(JSRuntimeBase jsRuntimeBase, string arguments) + internal static void ParseEndInvokeArguments(JSRuntime jsRuntimeBase, string arguments) { var utf8JsonBytes = Encoding.UTF8.GetBytes(arguments); diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs similarity index 77% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs index 71bfa28ad5..c077ac0b17 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs @@ -5,13 +5,13 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.JSInterop +namespace Microsoft.JSInterop.Infrastructure { - internal sealed class DotNetObjectReferenceJsonConverter : JsonConverter> where TValue : class + internal sealed class DotNetObjectReferenceJsonConverter : JsonConverter> where TValue : class { private static JsonEncodedText DotNetObjectRefKey => DotNetDispatcher.DotNetObjectRefKey; - public override DotNetObjectRef Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override DotNetObjectReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { long dotNetObjectId = 0; @@ -40,11 +40,11 @@ namespace Microsoft.JSInterop throw new JsonException($"Required property {DotNetObjectRefKey} not found."); } - var referenceManager = DotNetObjectRefManager.Current; - return (DotNetObjectRef)referenceManager.FindDotNetObject(dotNetObjectId); + var referenceManager = DotNetObjectReferenceManager.Current; + return (DotNetObjectReference)referenceManager.FindDotNetObject(dotNetObjectId); } - public override void Write(Utf8JsonWriter writer, DotNetObjectRef value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, DotNetObjectReference value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteNumber(DotNetObjectRefKey, value.ObjectId); diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs similarity index 90% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs index 5cfec5be9d..350530b624 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs @@ -5,13 +5,13 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.JSInterop +namespace Microsoft.JSInterop.Infrastructure { internal sealed class DotNetObjectReferenceJsonConverterFactory : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { - return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectRef<>); + return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectReference<>); } public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions jsonSerializerOptions) diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs similarity index 74% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs index a6be6aeb4f..709dd963fa 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs @@ -5,27 +5,27 @@ using System; using System.Collections.Concurrent; using System.Threading; -namespace Microsoft.JSInterop +namespace Microsoft.JSInterop.Infrastructure { - internal class DotNetObjectRefManager + 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(); + private readonly ConcurrentDictionary _trackedRefsById = new ConcurrentDictionary(); - public static DotNetObjectRefManager Current + public static DotNetObjectReferenceManager Current { get { - if (!(JSRuntime.Current is JSRuntimeBase jsRuntimeBase)) + if (!(JSRuntime.Current is JSRuntime jsRuntime)) { - throw new InvalidOperationException("JSRuntime must be set up correctly and must be an instance of JSRuntimeBase to use DotNetObjectRef."); + throw new InvalidOperationException("JSRuntime must be set up correctly and must be an instance of JSRuntimeBase to use DotNetObjectReference."); } - return jsRuntimeBase.ObjectRefManager; + return jsRuntime.ObjectRefManager; } } - public long TrackObject(IDotNetObjectRef dotNetObjectRef) + public long TrackObject(IDotNetObjectReference dotNetObjectRef) { var dotNetObjectId = Interlocked.Increment(ref _nextId); _trackedRefsById[dotNetObjectId] = dotNetObjectRef; @@ -33,7 +33,7 @@ namespace Microsoft.JSInterop return dotNetObjectId; } - public IDotNetObjectRef FindDotNetObject(long dotNetObjectId) + public IDotNetObjectReference FindDotNetObject(long dotNetObjectId) { return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) ? dotNetObjectRef @@ -45,7 +45,7 @@ namespace Microsoft.JSInterop /// 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 . + /// The ID of the . public void ReleaseDotNetObject(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/IDotNetObjectReference.cs similarity index 68% rename from src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/IDotNetObjectReference.cs index da16fa60a0..4b84f2bd0c 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/IDotNetObjectReference.cs @@ -3,9 +3,9 @@ using System; -namespace Microsoft.JSInterop +namespace Microsoft.JSInterop.Infrastructure { - internal interface IDotNetObjectRef : IDisposable + internal interface IDotNetObjectReference : IDisposable { object Value { get; } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/TaskGenericsUtil.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs similarity index 98% rename from src/JSInterop/Microsoft.JSInterop/src/TaskGenericsUtil.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs index 734e9863b8..4e14d50783 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/TaskGenericsUtil.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/TaskGenericsUtil.cs @@ -6,7 +6,7 @@ using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; -namespace Microsoft.JSInterop +namespace Microsoft.JSInterop.Infrastructure { internal static class TaskGenericsUtil { diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs similarity index 95% rename from src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs rename to src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs index 7606f1865f..cf8cc7030b 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs @@ -8,7 +8,7 @@ namespace Microsoft.JSInterop /// /// Abstract base class for an in-process JavaScript runtime. /// - public abstract class JSInProcessRuntimeBase : JSRuntimeBase, IJSInProcessRuntime + public abstract class JSInProcessRuntime : JSRuntime, IJSInProcessRuntime { /// /// Invokes the specified JavaScript function synchronously. diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs new file mode 100644 index 0000000000..73bc247848 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeExtensions.cs @@ -0,0 +1,29 @@ +// 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; + +namespace Microsoft.JSInterop +{ + /// + /// Extensions for . + /// + public static class JSInProcessRuntimeExtensions + { + /// + /// Invokes the specified JavaScript function synchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + public static void InvokeVoid(this IJSInProcessRuntime jsRuntime, string identifier, params object[] args) + { + if (jsRuntime == null) + { + throw new ArgumentNullException(nameof(jsRuntime)); + } + + jsRuntime.Invoke(identifier, args); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInvokableAttribute.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInvokableAttribute.cs index e037078cba..b710d54b2c 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInvokableAttribute.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInvokableAttribute.cs @@ -11,7 +11,7 @@ namespace Microsoft.JSInterop /// from untrusted callers. All inputs should be validated carefully. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - public class JSInvokableAttribute : Attribute + public sealed class JSInvokableAttribute : Attribute { /// /// Gets the identifier for the method. The identifier must be unique within the scope diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index ae097ca68e..598b47c4d4 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -2,19 +2,38 @@ // 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.Linq; +using System.Text.Json; using System.Threading; +using System.Threading.Tasks; +using Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop { /// - /// Provides mechanisms for accessing the current . + /// Abstract base class for a JavaScript runtime. /// - public static class JSRuntime + public abstract partial class JSRuntime : IJSRuntime { private static readonly AsyncLocal _currentJSRuntime = new AsyncLocal(); internal static IJSRuntime Current => _currentJSRuntime.Value; + private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" + private readonly ConcurrentDictionary _pendingTasks + = new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _cancellationRegistrations = + new ConcurrentDictionary(); + + internal DotNetObjectReferenceManager ObjectRefManager { get; } = new DotNetObjectReferenceManager(); + + /// + /// Gets or sets the default timeout for asynchronous JavaScript calls. + /// + protected TimeSpan? DefaultAsyncTimeout { get; set; } + /// /// Sets the current JS runtime to the supplied instance. /// @@ -26,5 +45,151 @@ namespace Microsoft.JSInterop _currentJSRuntime.Value = instance ?? throw new ArgumentNullException(nameof(instance)); } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different, or no timeout, + /// consider using . + /// + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public ValueTask InvokeAsync(string identifier, object[] args) + { + if (DefaultAsyncTimeout.HasValue) + { + return InvokeWithDefaultCancellation(identifier, args); + } + + return InvokeAsync(identifier, CancellationToken.None, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) + { + var taskId = Interlocked.Increment(ref _nextPendingTaskId); + var tcs = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); + if (cancellationToken != default) + { + _cancellationRegistrations[taskId] = cancellationToken.Register(() => + { + tcs.TrySetCanceled(cancellationToken); + CleanupTasksAndRegistrations(taskId); + }); + } + _pendingTasks[taskId] = tcs; + + try + { + if (cancellationToken.IsCancellationRequested) + { + tcs.TrySetCanceled(cancellationToken); + CleanupTasksAndRegistrations(taskId); + + return new ValueTask(tcs.Task); + } + + var argsJson = args?.Any() == true ? + JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options) : + null; + BeginInvokeJS(taskId, identifier, argsJson); + + return new ValueTask(tcs.Task); + } + catch + { + CleanupTasksAndRegistrations(taskId); + throw; + } + } + + private void CleanupTasksAndRegistrations(long taskId) + { + _pendingTasks.TryRemove(taskId, out _); + if (_cancellationRegistrations.TryRemove(taskId, out var registration)) + { + registration.Dispose(); + } + } + + private async ValueTask InvokeWithDefaultCancellation(string identifier, object[] args) + { + using (var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value)) + { + // We need to await here due to the using + return await InvokeAsync(identifier, cts.Token, args); + } + } + + /// + /// Begins an asynchronous function invocation. + /// + /// The identifier for the function invocation, or zero if no async callback is required. + /// The identifier for the function to invoke. + /// A JSON representation of the arguments. + protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); + + /// + /// Completes an async JS interop call from JavaScript to .NET + /// + /// The id of the JavaScript callback to execute on completion. + /// Whether the operation succeeded or not. + /// The result of the operation or an object containing error details. + /// The name of the method assembly if the invocation was for a static method. + /// The identifier for the method within the assembly. + /// The tracking id of the dotnet object if the invocation was for an instance method. + protected internal abstract void EndInvokeDotNet( + string callId, + bool success, + object resultOrError, + string assemblyName, + string methodIdentifier, + long dotNetObjectId); + + internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader) + { + if (!_pendingTasks.TryRemove(taskId, out var tcs)) + { + // We should simply return if we can't find an id for the invocation. + // This likely means that the method that initiated the call defined a timeout and stopped waiting. + return; + } + + CleanupTasksAndRegistrations(taskId); + + try + { + if (succeeded) + { + var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); + + var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptionsProvider.Options); + TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); + } + else + { + var exceptionText = jsonReader.GetString() ?? string.Empty; + TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(exceptionText)); + } + } + catch (Exception exception) + { + var message = $"An exception occurred executing JS interop: {exception.Message}. See InnerException for more details."; + TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception)); + } + } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs deleted file mode 100644 index 2121df0523..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs +++ /dev/null @@ -1,174 +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.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.JSInterop -{ - /// - /// Abstract base class for a JavaScript runtime. - /// - public abstract class JSRuntimeBase : IJSRuntime - { - private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" - private readonly ConcurrentDictionary _pendingTasks - = new ConcurrentDictionary(); - - private readonly ConcurrentDictionary _cancellationRegistrations = - new ConcurrentDictionary(); - - internal DotNetObjectRefManager ObjectRefManager { get; } = new DotNetObjectRefManager(); - - /// - /// Gets or sets the default timeout for asynchronous JavaScript calls. - /// - protected TimeSpan? DefaultAsyncTimeout { get; set; } - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// JSON-serializable arguments. - /// A cancellation token to signal the cancellation of the operation. - /// An instance of obtained by JSON-deserializing the return value. - public Task InvokeAsync(string identifier, IEnumerable args, CancellationToken cancellationToken = default) - { - var taskId = Interlocked.Increment(ref _nextPendingTaskId); - var tcs = new TaskCompletionSource(TaskContinuationOptions.RunContinuationsAsynchronously); - if (cancellationToken != default) - { - _cancellationRegistrations[taskId] = cancellationToken.Register(() => - { - tcs.TrySetCanceled(cancellationToken); - CleanupTasksAndRegistrations(taskId); - }); - } - _pendingTasks[taskId] = tcs; - - try - { - if (cancellationToken.IsCancellationRequested) - { - tcs.TrySetCanceled(cancellationToken); - CleanupTasksAndRegistrations(taskId); - - return tcs.Task; - } - - var argsJson = args?.Any() == true ? - JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options) : - null; - BeginInvokeJS(taskId, identifier, argsJson); - - return tcs.Task; - } - catch - { - CleanupTasksAndRegistrations(taskId); - throw; - } - } - - private void CleanupTasksAndRegistrations(long taskId) - { - _pendingTasks.TryRemove(taskId, out _); - if (_cancellationRegistrations.TryRemove(taskId, out var registration)) - { - registration.Dispose(); - } - } - - /// - /// Invokes the specified JavaScript function asynchronously. - /// - /// The JSON-serializable return type. - /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. - /// JSON-serializable arguments. - /// An instance of obtained by JSON-deserializing the return value. - public Task InvokeAsync(string identifier, params object[] args) - { - if (!DefaultAsyncTimeout.HasValue) - { - return InvokeAsync(identifier, args, default); - } - else - { - return InvokeWithDefaultCancellation(identifier, args); - } - } - - private async Task InvokeWithDefaultCancellation(string identifier, IEnumerable args) - { - using (var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value)) - { - // We need to await here due to the using - return await InvokeAsync(identifier, args, cts.Token); - } - } - - /// - /// Begins an asynchronous function invocation. - /// - /// The identifier for the function invocation, or zero if no async callback is required. - /// The identifier for the function to invoke. - /// A JSON representation of the arguments. - protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); - - /// - /// Completes an async JS interop call from JavaScript to .NET - /// - /// The id of the JavaScript callback to execute on completion. - /// Whether the operation succeeded or not. - /// The result of the operation or an object containing error details. - /// The name of the method assembly if the invocation was for a static method. - /// The identifier for the method within the assembly. - /// The tracking id of the dotnet object if the invocation was for an instance method. - protected internal abstract void EndInvokeDotNet( - string callId, - bool success, - object resultOrError, - string assemblyName, - string methodIdentifier, - long dotNetObjectId); - - internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader) - { - if (!_pendingTasks.TryRemove(taskId, out var tcs)) - { - // We should simply return if we can't find an id for the invocation. - // This likely means that the method that initiated the call defined a timeout and stopped waiting. - return; - } - - CleanupTasksAndRegistrations(taskId); - - try - { - if (succeeded) - { - var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); - - var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptionsProvider.Options); - TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); - } - else - { - var exceptionText = jsonReader.GetString() ?? string.Empty; - TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(exceptionText)); - } - } - catch (Exception exception) - { - var message = $"An exception occurred executing JS interop: {exception.Message}. See InnerException for more details."; - TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception)); - } - } - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs new file mode 100644 index 0000000000..ff4d3fd152 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeExtensions.cs @@ -0,0 +1,140 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.JSInterop +{ + /// + /// Extensions for . + /// + public static class JSRuntimeExtensions + { + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, params object[] args) + { + if (jsRuntime is null) + { + throw new ArgumentNullException(nameof(jsRuntime)); + } + + await jsRuntime.InvokeAsync(identifier, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// will apply timeouts to this operation based on the value configured in . To dispatch a call with a different timeout, or no timeout, + /// consider using . + /// + /// + /// The . + /// The JSON-serializable return type. + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public static ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, params object[] args) + { + if (jsRuntime is null) + { + throw new ArgumentNullException(nameof(jsRuntime)); + } + + return jsRuntime.InvokeAsync(identifier, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The JSON-serializable return type. + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// An instance of obtained by JSON-deserializing the return value. + public static ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args) + { + if (jsRuntime is null) + { + throw new ArgumentNullException(nameof(jsRuntime)); + } + + return jsRuntime.InvokeAsync(identifier, cancellationToken, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// + /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts + /// () from being applied. + /// + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, CancellationToken cancellationToken, params object[] args) + { + if (jsRuntime is null) + { + throw new ArgumentNullException(nameof(jsRuntime)); + } + + await jsRuntime.InvokeAsync(identifier, cancellationToken, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The duration after which to cancel the async operation. Overrides default timeouts (). + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object[] args) + { + if (jsRuntime is null) + { + throw new ArgumentNullException(nameof(jsRuntime)); + } + + + using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); + var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; + + return await jsRuntime.InvokeAsync(identifier, cancellationToken, args); + } + + /// + /// Invokes the specified JavaScript function asynchronously. + /// + /// The . + /// An identifier for the function to invoke. For example, the value "someScope.someFunction" will invoke the function window.someScope.someFunction. + /// The duration after which to cancel the async operation. Overrides default timeouts (). + /// JSON-serializable arguments. + /// A that represents the asynchronous invocation operation. + public static async ValueTask InvokeVoidAsync(this IJSRuntime jsRuntime, string identifier, TimeSpan timeout, params object[] args) + { + if (jsRuntime is null) + { + throw new ArgumentNullException(nameof(jsRuntime)); + } + + using var cancellationTokenSource = timeout == Timeout.InfiniteTimeSpan ? null : new CancellationTokenSource(timeout); + var cancellationToken = cancellationTokenSource?.Token ?? CancellationToken.None; + + await jsRuntime.InvokeAsync(identifier, cancellationToken, args); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs similarity index 83% rename from src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs rename to src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs index 22cb471f28..bcd5c95028 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs @@ -8,20 +8,20 @@ using static Microsoft.JSInterop.TestJSRuntime; namespace Microsoft.JSInterop { - public class DotNetObjectRefTest + public class DotNetObjectReferenceTest { [Fact] public Task CanAccessValue() => WithJSRuntime(_ => { var obj = new object(); - Assert.Same(obj, DotNetObjectRef.Create(obj).Value); + Assert.Same(obj, DotNetObjectReference.Create(obj).Value); }); [Fact] public Task NotifiesAssociatedJsRuntimeOfDisposal() => WithJSRuntime(jsRuntime => { // Arrange - var objRef = DotNetObjectRef.Create(new object()); + var objRef = DotNetObjectReference.Create(new object()); // Act Assert.Equal(1, objRef.ObjectId); diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs similarity index 91% rename from src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs rename to src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index 13e01c2304..d9ddac2a89 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -namespace Microsoft.JSInterop +namespace Microsoft.JSInterop.Infrastructure { public class DotNetDispatcherTest { @@ -114,7 +114,7 @@ namespace Microsoft.JSInterop { // Arrange: Track a .NET object to use as an arg var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; - var objectRef = DotNetObjectRef.Create(arg3); + var objectRef = DotNetObjectReference.Create(arg3); jsRuntime.Invoke("unimportant", objectRef); // Arrange: Remaining args @@ -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>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64())).Value; + var resultDto2 = Assert.IsType>(DotNetObjectReferenceManager.Current.FindDotNetObject(property.GetInt64())).Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(1299, resultDto2.IntVal); }); @@ -153,7 +153,7 @@ namespace Microsoft.JSInterop // Arrange var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage); var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; - var objectRef = DotNetObjectRef.Create(arg3); + var objectRef = DotNetObjectReference.Create(arg3); jsRuntime.Invoke("unimportant", objectRef); // Arrange: Remaining args @@ -175,7 +175,7 @@ namespace Microsoft.JSInterop { // Arrange: Track some instance var targetInstance = new SomePublicType(); - var objectRef = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); // Act @@ -191,7 +191,7 @@ namespace Microsoft.JSInterop { // Arrange: Track some instance var targetInstance = new DerivedClass(); - var objectRef = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); // Act @@ -207,10 +207,10 @@ namespace Microsoft.JSInterop { // Arrange var targetInstance = new SomePublicType(); - var objectRef = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); // Act - DotNetDispatcher.BeginInvoke(null, null, "__Dispose", objectRef.ObjectId, null); + DotNetDispatcher.BeginInvokeDotNet(null, null, "__Dispose", objectRef.ObjectId, null); // Assert Assert.True(objectRef.Disposed); @@ -224,7 +224,7 @@ namespace Microsoft.JSInterop // Arrange: Track some instance, then dispose it var targetInstance = new SomePublicType(); - var objectRef = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); objectRef.Dispose(); @@ -242,7 +242,7 @@ namespace Microsoft.JSInterop // Arrange: Track some instance, then dispose it var targetInstance = new SomePublicType(); - var objectRef = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); objectRef.Dispose(); @@ -261,10 +261,10 @@ namespace Microsoft.JSInterop var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, JsonSerializerOptionsProvider.Options); // Act - DotNetDispatcher.EndInvoke(argsJson); + DotNetDispatcher.EndInvokeJS(argsJson); // Assert - Assert.True(task.IsCompleted && task.Status == TaskStatus.RanToCompletion); + Assert.True(task.IsCompletedSuccessfully); var result = task.Result; Assert.Equal(testDTO.StringVal, result.StringVal); Assert.Equal(testDTO.IntVal, result.IntVal); @@ -279,10 +279,10 @@ namespace Microsoft.JSInterop var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, JsonSerializerOptionsProvider.Options); // Act - DotNetDispatcher.EndInvoke(argsJson); + DotNetDispatcher.EndInvokeJS(argsJson); // Assert - var ex = await Assert.ThrowsAsync(() => task); + var ex = await Assert.ThrowsAsync(async () => await task); Assert.Equal(expected, ex.Message); }); @@ -297,7 +297,7 @@ namespace Microsoft.JSInterop // Act cts.Cancel(); - DotNetDispatcher.EndInvoke(argsJson); + DotNetDispatcher.EndInvokeJS(argsJson); // Assert Assert.True(task.IsCanceled); @@ -311,10 +311,10 @@ namespace Microsoft.JSInterop var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, JsonSerializerOptionsProvider.Options); // Act - DotNetDispatcher.EndInvoke(argsJson); + DotNetDispatcher.EndInvokeJS(argsJson); // Assert - var ex = await Assert.ThrowsAsync(() => task); + var ex = await Assert.ThrowsAsync(async () => await task); Assert.Empty(ex.Message); }); @@ -325,8 +325,8 @@ namespace Microsoft.JSInterop var targetInstance = new SomePublicType(); var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; jsRuntime.Invoke("unimportant", - DotNetObjectRef.Create(targetInstance), - DotNetObjectRef.Create(arg2)); + DotNetObjectReference.Create(targetInstance), + DotNetObjectReference.Create(arg2)); var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]"; // Act @@ -334,7 +334,7 @@ namespace Microsoft.JSInterop // Assert Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson); - var resultDto = ((DotNetObjectRef)jsRuntime.ObjectRefManager.FindDotNetObject(3)).Value; + var resultDto = ((DotNetObjectReference)jsRuntime.ObjectRefManager.FindDotNetObject(3)).Value; Assert.Equal(1235, resultDto.IntVal); Assert.Equal("MY STRING", resultDto.StringVal); }); @@ -362,7 +362,7 @@ namespace Microsoft.JSInterop public Task CannotInvokeWithMoreParameters() => WithJSRuntime(jsRuntime => { // Arrange - var objectRef = DotNetObjectRef.Create(new TestDTO { IntVal = 4 }); + var objectRef = DotNetObjectReference.Create(new TestDTO { IntVal = 4 }); var argsJson = JsonSerializer.Serialize(new object[] { new TestDTO { StringVal = "Another string", IntVal = 456 }, @@ -386,8 +386,8 @@ namespace Microsoft.JSInterop // Arrange: Track some instance plus another object we'll pass as a param var targetInstance = new SomePublicType(); var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; - var arg1Ref = DotNetObjectRef.Create(targetInstance); - var arg2Ref = DotNetObjectRef.Create(arg2); + var arg1Ref = DotNetObjectReference.Create(targetInstance); + var arg2Ref = DotNetObjectReference.Create(arg2); jsRuntime.Invoke("unimportant", arg1Ref, arg2Ref); // Arrange: all args @@ -400,7 +400,7 @@ namespace Microsoft.JSInterop // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvoke(callId, null, "InvokableAsyncMethod", 1, argsJson); + DotNetDispatcher.BeginInvokeDotNet(callId, null, "InvokableAsyncMethod", 1, argsJson); await resultTask; // Assert: Correct completion information @@ -413,7 +413,7 @@ namespace Microsoft.JSInterop Assert.Equal(2000, resultDto1.IntVal); // Assert: Second result value marshalled by ref - var resultDto2Ref = Assert.IsType>(result[1]); + var resultDto2Ref = Assert.IsType>(result[1]); var resultDto2 = resultDto2Ref.Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(2468, resultDto2.IntVal); @@ -427,7 +427,7 @@ namespace Microsoft.JSInterop // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvoke(callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -449,7 +449,7 @@ namespace Microsoft.JSInterop // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvoke(callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -469,7 +469,7 @@ namespace Microsoft.JSInterop // Arrange var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvoke(callId, thisAssemblyName, "InvocableStaticWithParams", default, "not json"); + DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, "InvocableStaticWithParams", default, "not json"); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -486,7 +486,7 @@ namespace Microsoft.JSInterop // Arrange var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvoke(callId, null, "InvokableInstanceVoid", 1, null); + DotNetDispatcher.BeginInvokeDotNet(callId, null, "InvokableInstanceVoid", 1, null); // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); @@ -611,7 +611,7 @@ namespace Microsoft.JSInterop DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]"); - Assert.True(task.IsCompleted && task.Status == TaskStatus.RanToCompletion); + Assert.True(task.IsCompletedSuccessfully); Assert.Equal(7, task.Result.IntVal); } @@ -623,7 +623,7 @@ namespace Microsoft.JSInterop DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]"); - Assert.True(task.IsCompleted && task.Status == TaskStatus.RanToCompletion); + Assert.True(task.IsCompletedSuccessfully); Assert.Equal(new[] { 1, 2, 3 }, task.Result); } @@ -635,7 +635,7 @@ namespace Microsoft.JSInterop DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]"); - Assert.True(task.IsCompleted && task.Status == TaskStatus.RanToCompletion); + Assert.True(task.IsCompletedSuccessfully); Assert.Null(task.Result); } @@ -685,7 +685,7 @@ namespace Microsoft.JSInterop => new TestDTO { StringVal = "Test", IntVal = 123 }; [JSInvokable("InvocableStaticWithParams")] - public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, DotNetObjectRef dtoByRef) + public static object[] MyInvocableWithParams(TestDTO dtoViaJson, int[] incrementAmounts, DotNetObjectReference dtoByRef) => new object[] { new TestDTO // Return via JSON marshalling @@ -693,7 +693,7 @@ namespace Microsoft.JSInterop StringVal = dtoViaJson.StringVal.ToUpperInvariant(), IntVal = dtoViaJson.IntVal + incrementAmounts.Sum() }, - DotNetObjectRef.Create(new TestDTO // Return by ref + DotNetObjectReference.Create(new TestDTO // Return by ref { StringVal = dtoByRef.Value.StringVal.ToUpperInvariant(), IntVal = dtoByRef.Value.IntVal + incrementAmounts.Sum() @@ -715,7 +715,7 @@ namespace Microsoft.JSInterop } [JSInvokable] - public object[] InvokableInstanceMethod(string someString, DotNetObjectRef someDTORef) + public object[] InvokableInstanceMethod(string someString, DotNetObjectReference someDTORef) { var someDTO = someDTORef.Value; // Returning an array to make the point that object references @@ -723,7 +723,7 @@ namespace Microsoft.JSInterop return new object[] { $"You passed {someString}", - DotNetObjectRef.Create(new TestDTO + DotNetObjectReference.Create(new TestDTO { IntVal = someDTO.IntVal + 1, StringVal = someDTO.StringVal.ToUpperInvariant() @@ -732,7 +732,7 @@ namespace Microsoft.JSInterop } [JSInvokable] - public async Task InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectRef dtoByRefWrapper) + public async Task InvokableAsyncMethod(TestDTO dtoViaJson, DotNetObjectReference dtoByRefWrapper) { await Task.Delay(50); var dtoByRef = dtoByRefWrapper.Value; @@ -743,7 +743,7 @@ namespace Microsoft.JSInterop StringVal = dtoViaJson.StringVal.ToUpperInvariant(), IntVal = dtoViaJson.IntVal * 2, }, - DotNetObjectRef.Create(new TestDTO // Return by ref + DotNetObjectReference.Create(new TestDTO // Return by ref { StringVal = dtoByRef.StringVal.ToUpperInvariant(), IntVal = dtoByRef.IntVal * 2, @@ -789,7 +789,7 @@ namespace Microsoft.JSInterop } } - public class TestJSRuntime : JSInProcessRuntimeBase + public class TestJSRuntime : JSInProcessRuntime { private TaskCompletionSource _nextInvocationTcs = new TaskCompletionSource(); public Task NextInvocationTask => _nextInvocationTcs.Task; diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs similarity index 80% rename from src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs rename to src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs index 18f3db55c1..541ad2b025 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Xunit; using static Microsoft.JSInterop.TestJSRuntime; -namespace Microsoft.JSInterop.Tests +namespace Microsoft.JSInterop.Infrastructure { public class DotNetObjectReferenceJsonConverterTest { @@ -14,12 +14,12 @@ namespace Microsoft.JSInterop.Tests public Task Read_Throws_IfJsonIsMissingDotNetObjectProperty() => WithJSRuntime(_ => { // Arrange - var dotNetObjectRef = DotNetObjectRef.Create(new TestModel()); + var dotNetObjectRef = DotNetObjectReference.Create(new TestModel()); var json = "{}"; // Act & Assert - var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json)); + var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json)); Assert.Equal("Required property __dotNetObject not found.", ex.Message); }); @@ -27,12 +27,12 @@ namespace Microsoft.JSInterop.Tests public Task Read_Throws_IfJsonContainsUnknownContent() => WithJSRuntime(_ => { // Arrange - var dotNetObjectRef = DotNetObjectRef.Create(new TestModel()); + 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)); Assert.Equal("Unexcepted JSON property foo.", ex.Message); }); @@ -41,13 +41,13 @@ namespace Microsoft.JSInterop.Tests { // Arrange var input = new TestModel(); - var dotNetObjectRef = DotNetObjectRef.Create(input); + var dotNetObjectRef = DotNetObjectReference.Create(input); var objectId = dotNetObjectRef.ObjectId; var json = $"{{\"__dotNetObject\":{objectId}"; // Act & Assert - var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); + var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); Assert.IsAssignableFrom(ex); }); @@ -56,13 +56,13 @@ namespace Microsoft.JSInterop.Tests { // Arrange var input = new TestModel(); - var dotNetObjectRef = DotNetObjectRef.Create(input); + var dotNetObjectRef = DotNetObjectReference.Create(input); var objectId = dotNetObjectRef.ObjectId; var json = $"{{\"__dotNetObject\":{objectId},\"__dotNetObject\":{objectId}}}"; // Act & Assert - var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); + var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); Assert.IsAssignableFrom(ex); }); @@ -71,13 +71,13 @@ namespace Microsoft.JSInterop.Tests { // Arrange var input = new TestModel(); - var dotNetObjectRef = DotNetObjectRef.Create(input); + var dotNetObjectRef = DotNetObjectReference.Create(input); var objectId = dotNetObjectRef.ObjectId; var json = $"{{\"__dotNetObject\":{objectId}}}"; // Act - var deserialized = JsonSerializer.Deserialize>(json); + var deserialized = JsonSerializer.Deserialize>(json); // Assert Assert.Same(input, deserialized.Value); @@ -92,13 +92,13 @@ namespace Microsoft.JSInterop.Tests // Track a few instances and verify that the deserialized value returns the correct value. var instance1 = new TestModel(); var instance2 = new TestModel(); - var ref1 = DotNetObjectRef.Create(instance1); - var ref2 = DotNetObjectRef.Create(instance2); + var ref1 = DotNetObjectReference.Create(instance1); + var ref2 = DotNetObjectReference.Create(instance2); var json = $"[{{\"__dotNetObject\":{ref2.ObjectId}}},{{\"__dotNetObject\":{ref1.ObjectId}}}]"; // Act - var deserialized = JsonSerializer.Deserialize[]>(json); + var deserialized = JsonSerializer.Deserialize[]>(json); // Assert Assert.Same(instance2, deserialized[0].Value); @@ -110,7 +110,7 @@ namespace Microsoft.JSInterop.Tests { // Arrange var input = new TestModel(); - var dotNetObjectRef = DotNetObjectRef.Create(input); + var dotNetObjectRef = DotNetObjectReference.Create(input); var objectId = dotNetObjectRef.ObjectId; var json = @@ -119,7 +119,7 @@ namespace Microsoft.JSInterop.Tests }}"; // Act - var deserialized = JsonSerializer.Deserialize>(json); + var deserialized = JsonSerializer.Deserialize>(json); // Assert Assert.Same(input, deserialized.Value); @@ -130,7 +130,7 @@ namespace Microsoft.JSInterop.Tests public Task WriteJsonTwice_KeepsObjectId() => WithJSRuntime(_ => { // Arrange - var dotNetObjectRef = DotNetObjectRef.Create(new TestModel()); + var dotNetObjectRef = DotNetObjectReference.Create(new TestModel()); // Act var json1 = JsonSerializer.Serialize(dotNetObjectRef); diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeExtensionsTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeExtensionsTest.cs new file mode 100644 index 0000000000..3a7f0a4d79 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeExtensionsTest.cs @@ -0,0 +1,27 @@ +// 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.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.JSInterop +{ + public class JSInProcessRuntimeExtensionsTest + { + [Fact] + public void InvokeVoid_Works() + { + // Arrange + var method = "someMethod"; + var args = new[] { "a", "b" }; + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.Invoke(method, args)).Returns(new ValueTask(new object())); + + // Act + jsRuntime.Object.InvokeVoid(method, args); + + jsRuntime.Verify(); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs similarity index 88% rename from src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs rename to src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs index d71969d450..4054101258 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.JSInterop.Tests +namespace Microsoft.JSInterop { public class JSInProcessRuntimeBaseTest { @@ -43,12 +43,12 @@ namespace Microsoft.JSInterop.Tests // Act // Showing we can pass the DotNetObject either as top-level args or nested - var syncResult = runtime.Invoke>("test identifier", - DotNetObjectRef.Create(obj1), + var syncResult = runtime.Invoke>("test identifier", + DotNetObjectReference.Create(obj1), new Dictionary { - { "obj2", DotNetObjectRef.Create(obj2) }, - { "obj3", DotNetObjectRef.Create(obj3) }, + { "obj2", DotNetObjectReference.Create(obj2) }, + { "obj3", DotNetObjectReference.Create(obj3) }, }); // Assert: Handles null result string @@ -78,11 +78,11 @@ namespace Microsoft.JSInterop.Tests var obj2 = new object(); // Act - var syncResult = runtime.Invoke[]>( + var syncResult = runtime.Invoke[]>( "test identifier", - DotNetObjectRef.Create(obj1), + DotNetObjectReference.Create(obj1), "some other arg", - DotNetObjectRef.Create(obj2)); + DotNetObjectReference.Create(obj2)); var call = runtime.InvokeCalls.Single(); // Assert @@ -95,7 +95,7 @@ namespace Microsoft.JSInterop.Tests public string StringValue { get; set; } } - class TestJSInProcessRuntime : JSInProcessRuntimeBase + class TestJSInProcessRuntime : JSInProcessRuntime { public List InvokeCalls { get; set; } = new List(); diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs deleted file mode 100644 index c3bf4f9eef..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs +++ /dev/null @@ -1,386 +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.Generic; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.JSInterop -{ - public class JSRuntimeBaseTest - { - [Fact] - public void DispatchesAsyncCallsWithDistinctAsyncHandles() - { - // Arrange - var runtime = new TestJSRuntime(); - - // Act - runtime.InvokeAsync("test identifier 1", "arg1", 123, true); - runtime.InvokeAsync("test identifier 2", "some other arg"); - - // Assert - Assert.Collection(runtime.BeginInvokeCalls, - call => - { - Assert.Equal("test identifier 1", call.Identifier); - Assert.Equal("[\"arg1\",123,true]", call.ArgsJson); - }, - call => - { - Assert.Equal("test identifier 2", call.Identifier); - Assert.Equal("[\"some other arg\"]", call.ArgsJson); - Assert.NotEqual(runtime.BeginInvokeCalls[0].AsyncHandle, call.AsyncHandle); - }); - } - - [Fact] - public async Task InvokeAsync_CancelsAsyncTask_AfterDefaultTimeout() - { - // Arrange - var runtime = new TestJSRuntime(); - runtime.DefaultTimeout = TimeSpan.FromSeconds(1); - - // Act - var task = runtime.InvokeAsync("test identifier 1", "arg1", 123, true); - - // Assert - await Assert.ThrowsAsync(async () => await task); - } - - [Fact] - public void InvokeAsync_CompletesSuccessfullyBeforeTimeout() - { - // Arrange - var runtime = new TestJSRuntime(); - runtime.DefaultTimeout = TimeSpan.FromSeconds(10); - var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes("null")); - - // Act - var task = runtime.InvokeAsync("test identifier 1", "arg1", 123, true); - - runtime.EndInvokeJS(2, succeeded: true, ref reader); - - Assert.True(task.IsCompleted && task.Status == TaskStatus.RanToCompletion); - } - - [Fact] - public async Task InvokeAsync_CancelsAsyncTasksWhenCancellationTokenFires() - { - // Arrange - using var cts = new CancellationTokenSource(); - var runtime = new TestJSRuntime(); - - // Act - var task = runtime.InvokeAsync("test identifier 1", new object[] { "arg1", 123, true }, cts.Token); - - cts.Cancel(); - - // Assert - await Assert.ThrowsAsync(async () => await task); - } - - [Fact] - public async Task InvokeAsync_DoesNotStartWorkWhenCancellationHasBeenRequested() - { - // Arrange - using var cts = new CancellationTokenSource(); - cts.Cancel(); - var runtime = new TestJSRuntime(); - - // Act - var task = runtime.InvokeAsync("test identifier 1", new object[] { "arg1", 123, true }, cts.Token); - - cts.Cancel(); - - // Assert - await Assert.ThrowsAsync(async () => await task); - Assert.Empty(runtime.BeginInvokeCalls); - } - - [Fact] - public void CanCompleteAsyncCallsAsSuccess() - { - // Arrange - var runtime = new TestJSRuntime(); - - // Act/Assert: Tasks not initially completed - var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); - var task = runtime.InvokeAsync("test identifier", Array.Empty()); - Assert.False(unrelatedTask.IsCompleted); - Assert.False(task.IsCompleted); - var bytes = Encoding.UTF8.GetBytes("\"my result\""); - var reader = new Utf8JsonReader(bytes); - - // Act/Assert: Task can be completed - runtime.EndInvokeJS( - runtime.BeginInvokeCalls[1].AsyncHandle, - /* succeeded: */ true, - ref reader); - Assert.False(unrelatedTask.IsCompleted); - Assert.True(task.IsCompleted); - Assert.Equal("my result", task.Result); - } - - [Fact] - public void CanCompleteAsyncCallsWithComplexType() - { - // Arrange - var runtime = new TestJSRuntime(); - - var task = runtime.InvokeAsync("test identifier", Array.Empty()); - var bytes = Encoding.UTF8.GetBytes("{\"id\":10, \"name\": \"Test\"}"); - var reader = new Utf8JsonReader(bytes); - - // Act/Assert: Task can be completed - runtime.EndInvokeJS( - runtime.BeginInvokeCalls[0].AsyncHandle, - /* succeeded: */ true, - ref reader); - Assert.True(task.IsCompleted); - var poco = task.Result; - Assert.Equal(10, poco.Id); - Assert.Equal("Test", poco.Name); - } - - [Fact] - public void CanCompleteAsyncCallsWithComplexTypeUsingPropertyCasing() - { - // Arrange - var runtime = new TestJSRuntime(); - - var task = runtime.InvokeAsync("test identifier", Array.Empty()); - var bytes = Encoding.UTF8.GetBytes("{\"Id\":10, \"Name\": \"Test\"}"); - var reader = new Utf8JsonReader(bytes); - reader.Read(); - - // Act/Assert: Task can be completed - runtime.EndInvokeJS( - runtime.BeginInvokeCalls[0].AsyncHandle, - /* succeeded: */ true, - ref reader); - Assert.True(task.IsCompleted); - var poco = task.Result; - Assert.Equal(10, poco.Id); - Assert.Equal("Test", poco.Name); - } - - [Fact] - public void CanCompleteAsyncCallsAsFailure() - { - // Arrange - var runtime = new TestJSRuntime(); - - // Act/Assert: Tasks not initially completed - var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); - var task = runtime.InvokeAsync("test identifier", Array.Empty()); - Assert.False(unrelatedTask.IsCompleted); - Assert.False(task.IsCompleted); - var bytes = Encoding.UTF8.GetBytes("\"This is a test exception\""); - var reader = new Utf8JsonReader(bytes); - reader.Read(); - - // Act/Assert: Task can be failed - runtime.EndInvokeJS( - runtime.BeginInvokeCalls[1].AsyncHandle, - /* succeeded: */ false, - ref reader); - Assert.False(unrelatedTask.IsCompleted); - Assert.True(task.IsCompleted); - - Assert.IsType(task.Exception); - Assert.IsType(task.Exception.InnerException); - Assert.Equal("This is a test exception", ((JSException)task.Exception.InnerException).Message); - } - - [Fact] - public Task CanCompleteAsyncCallsWithErrorsDuringDeserialization() - { - // Arrange - var runtime = new TestJSRuntime(); - - // Act/Assert: Tasks not initially completed - var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); - var task = runtime.InvokeAsync("test identifier", Array.Empty()); - Assert.False(unrelatedTask.IsCompleted); - Assert.False(task.IsCompleted); - var bytes = Encoding.UTF8.GetBytes("Not a string"); - var reader = new Utf8JsonReader(bytes); - - // Act/Assert: Task can be failed - runtime.EndInvokeJS( - runtime.BeginInvokeCalls[1].AsyncHandle, - /* succeeded: */ true, - ref reader); - Assert.False(unrelatedTask.IsCompleted); - - return AssertTask(); - - async Task AssertTask() - { - var jsException = await Assert.ThrowsAsync(() => task); - Assert.IsAssignableFrom(jsException.InnerException); - } - } - - [Fact] - public Task CompletingSameAsyncCallMoreThanOnce_IgnoresSecondResultAsync() - { - // Arrange - var runtime = new TestJSRuntime(); - - // Act/Assert - var task = runtime.InvokeAsync("test identifier", Array.Empty()); - var asyncHandle = runtime.BeginInvokeCalls[0].AsyncHandle; - var firstReader = new Utf8JsonReader(Encoding.UTF8.GetBytes("\"Some data\"")); - var secondReader = new Utf8JsonReader(Encoding.UTF8.GetBytes("\"Exception\"")); - - runtime.EndInvokeJS(asyncHandle, true, ref firstReader); - runtime.EndInvokeJS(asyncHandle, false, ref secondReader); - - return AssertTask(); - - async Task AssertTask() - { - var result = await task; - Assert.Equal("Some data", result); - } - } - - [Fact] - public void SerializesDotNetObjectWrappersInKnownFormat() - { - // Arrange - var runtime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(runtime); - var obj1 = new object(); - var obj2 = new object(); - var obj3 = new object(); - - // Act - // Showing we can pass the DotNetObject either as top-level args or nested - var obj1Ref = DotNetObjectRef.Create(obj1); - var obj1DifferentRef = DotNetObjectRef.Create(obj1); - runtime.InvokeAsync("test identifier", - obj1Ref, - new Dictionary - { - { "obj2", DotNetObjectRef.Create(obj2) }, - { "obj3", DotNetObjectRef.Create(obj3) }, - { "obj1SameRef", obj1Ref }, - { "obj1DifferentRef", obj1DifferentRef }, - }); - - // 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: Objects were tracked - 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] - public void CanSanitizeDotNetInteropExceptions() - { - // Arrange - var expectedMessage = "An error ocurred while invoking '[Assembly]::Method'. Swapping to 'Development' environment will " + - "display more detailed information about the error that occurred."; - - string GetMessage(string assembly, string method) => $"An error ocurred while invoking '[{assembly}]::{method}'. Swapping to 'Development' environment will " + - "display more detailed information about the error that occurred."; - - var runtime = new TestJSRuntime() - { - OnDotNetException = (e, a, m) => new JSError { Message = GetMessage(a, m) } - }; - - var exception = new Exception("Some really sensitive data in here"); - - // Act - runtime.EndInvokeDotNet("0", false, exception, "Assembly", "Method", 0); - - // Assert - var call = runtime.EndInvokeDotNetCalls.Single(); - Assert.Equal("0", call.CallId); - Assert.False(call.Success); - var jsError = Assert.IsType(call.ResultOrError); - Assert.Equal(expectedMessage, jsError.Message); - } - - private class JSError - { - public string Message { get; set; } - } - - private class TestPoco - { - public int Id { get; set; } - - public string Name { get; set; } - } - - class TestJSRuntime : JSRuntimeBase - { - public List BeginInvokeCalls = new List(); - public List EndInvokeDotNetCalls = new List(); - - public TimeSpan? DefaultTimeout - { - set - { - base.DefaultAsyncTimeout = value; - } - } - - public class BeginInvokeAsyncArgs - { - public long AsyncHandle { get; set; } - public string Identifier { get; set; } - public string ArgsJson { get; set; } - } - - public class EndInvokeDotNetArgs - { - public string CallId { get; set; } - public bool Success { get; set; } - public object ResultOrError { get; set; } - } - - public Func OnDotNetException { get; set; } - - protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) - { - if (OnDotNetException != null && !success) - { - resultOrError = OnDotNetException(resultOrError as Exception, assemblyName, methodIdentifier); - } - - EndInvokeDotNetCalls.Add(new EndInvokeDotNetArgs - { - CallId = callId, - Success = success, - ResultOrError = resultOrError - }); - } - - protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) - { - BeginInvokeCalls.Add(new BeginInvokeAsyncArgs - { - AsyncHandle = asyncHandle, - Identifier = identifier, - ArgsJson = argsJson, - }); - } - } - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeExtensionsTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeExtensionsTest.cs new file mode 100644 index 0000000000..a5f69fbef2 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeExtensionsTest.cs @@ -0,0 +1,181 @@ +// 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.Threading; +using System.Threading.Tasks; +using Moq; +using Xunit; + +namespace Microsoft.JSInterop +{ + public class JSRuntimeExtensionsTest + { + [Fact] + public async Task InvokeAsync_WithParamsArgs() + { + // Arrange + var method = "someMethod"; + var expected = new[] { "a", "b" }; + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.InvokeAsync(method, It.IsAny())) + .Callback((method, args) => + { + Assert.Equal(expected, args); + }) + .Returns(new ValueTask("Hello")) + .Verifiable(); + + // Act + var result = await jsRuntime.Object.InvokeAsync(method, "a", "b"); + + // Assert + Assert.Equal("Hello", result); + jsRuntime.Verify(); + } + + [Fact] + public async Task InvokeAsync_WithParamsArgsAndCancellationToken() + { + // Arrange + var method = "someMethod"; + var expected = new[] { "a", "b" }; + var cancellationToken = new CancellationToken(); + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.InvokeAsync(method, cancellationToken, It.IsAny())) + .Callback((method, cts, args) => + { + Assert.Equal(expected, args); + }) + .Returns(new ValueTask("Hello")) + .Verifiable(); + + // Act + var result = await jsRuntime.Object.InvokeAsync(method, cancellationToken, "a", "b"); + + // Assert + Assert.Equal("Hello", result); + jsRuntime.Verify(); + } + + [Fact] + public async Task InvokeVoidAsync_WithoutCancellationToken() + { + // Arrange + var method = "someMethod"; + var args = new[] { "a", "b" }; + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.InvokeAsync(method, args)).Returns(new ValueTask(new object())); + + // Act + await jsRuntime.Object.InvokeVoidAsync(method, args); + + jsRuntime.Verify(); + } + + [Fact] + public async Task InvokeVoidAsync_WithCancellationToken() + { + // Arrange + var method = "someMethod"; + var args = new[] { "a", "b" }; + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.InvokeAsync(method, It.IsAny(), args)).Returns(new ValueTask(new object())); + + // Act + await jsRuntime.Object.InvokeVoidAsync(method, new CancellationToken(), args); + + jsRuntime.Verify(); + } + + [Fact] + public async Task InvokeAsync_WithTimeout() + { + // Arrange + var expected = "Hello"; + var method = "someMethod"; + var args = new[] { "a", "b" }; + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.InvokeAsync(method, It.IsAny(), args)) + .Callback((method, cts, args) => + { + // There isn't a very good way to test when the cts will cancel. We'll just verify that + // it'll get cancelled eventually. + Assert.True(cts.CanBeCanceled); + }) + .Returns(new ValueTask(expected)); + + // Act + var result = await jsRuntime.Object.InvokeAsync(method, TimeSpan.FromMinutes(5), args); + + Assert.Equal(expected, result); + jsRuntime.Verify(); + } + + [Fact] + public async Task InvokeAsync_WithInfiniteTimeout() + { + // Arrange + var expected = "Hello"; + var method = "someMethod"; + var args = new[] { "a", "b" }; + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.InvokeAsync(method, It.IsAny(), args)) + .Callback((method, cts, args) => + { + Assert.False(cts.CanBeCanceled); + Assert.True(cts == CancellationToken.None); + }) + .Returns(new ValueTask(expected)); + + // Act + var result = await jsRuntime.Object.InvokeAsync(method, Timeout.InfiniteTimeSpan, args); + + Assert.Equal(expected, result); + jsRuntime.Verify(); + } + + [Fact] + public async Task InvokeVoidAsync_WithTimeout() + { + // Arrange + var method = "someMethod"; + var args = new[] { "a", "b" }; + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.InvokeAsync(method, It.IsAny(), args)) + .Callback((method, cts, args) => + { + // There isn't a very good way to test when the cts will cancel. We'll just verify that + // it'll get cancelled eventually. + Assert.True(cts.CanBeCanceled); + }) + .Returns(new ValueTask(new object())); + + // Act + await jsRuntime.Object.InvokeVoidAsync(method, TimeSpan.FromMinutes(5), args); + + jsRuntime.Verify(); + } + + [Fact] + public async Task InvokeVoidAsync_WithInfiniteTimeout() + { + // Arrange + var method = "someMethod"; + var args = new[] { "a", "b" }; + var jsRuntime = new Mock(MockBehavior.Strict); + jsRuntime.Setup(s => s.InvokeAsync(method, It.IsAny(), args)) + .Callback((method, cts, args) => + { + Assert.False(cts.CanBeCanceled); + Assert.True(cts == CancellationToken.None); + }) + .Returns(new ValueTask(new object())); + + // Act + await jsRuntime.Object.InvokeVoidAsync(method, Timeout.InfiniteTimeSpan, args); + + jsRuntime.Verify(); + } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index f2fab5d741..4e65ddeb0f 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -4,20 +4,23 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Xunit; -namespace Microsoft.JSInterop.Tests +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 FakeJSRuntime(); + var jsRuntime = new TestJSRuntime(); JSRuntime.SetCurrentJSRuntime(jsRuntime); await Task.Delay(50).ConfigureAwait(false); Assert.Same(jsRuntime, JSRuntime.Current); @@ -26,14 +29,377 @@ namespace Microsoft.JSInterop.Tests await Task.WhenAll(tasks); Assert.Null(JSRuntime.Current); } + #endregion - private class FakeJSRuntime : IJSRuntime + [Fact] + public void DispatchesAsyncCallsWithDistinctAsyncHandles() { - public Task InvokeAsync(string identifier, params object[] args) - => throw new NotImplementedException(); + // Arrange + var runtime = new TestJSRuntime(); - public Task InvokeAsync(string identifier, IEnumerable args, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + // Act + runtime.InvokeAsync("test identifier 1", "arg1", 123, true); + runtime.InvokeAsync("test identifier 2", "some other arg"); + + // Assert + Assert.Collection(runtime.BeginInvokeCalls, + call => + { + Assert.Equal("test identifier 1", call.Identifier); + Assert.Equal("[\"arg1\",123,true]", call.ArgsJson); + }, + call => + { + Assert.Equal("test identifier 2", call.Identifier); + Assert.Equal("[\"some other arg\"]", call.ArgsJson); + Assert.NotEqual(runtime.BeginInvokeCalls[0].AsyncHandle, call.AsyncHandle); + }); + } + + [Fact] + public async Task InvokeAsync_CancelsAsyncTask_AfterDefaultTimeout() + { + // Arrange + var runtime = new TestJSRuntime(); + runtime.DefaultTimeout = TimeSpan.FromSeconds(1); + + // Act + var task = runtime.InvokeAsync("test identifier 1", "arg1", 123, true); + + // Assert + await Assert.ThrowsAsync(async () => await task); + } + + [Fact] + public void InvokeAsync_CompletesSuccessfullyBeforeTimeout() + { + // Arrange + var runtime = new TestJSRuntime(); + runtime.DefaultTimeout = TimeSpan.FromSeconds(10); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes("null")); + + // Act + var task = runtime.InvokeAsync("test identifier 1", "arg1", 123, true); + + runtime.EndInvokeJS(2, succeeded: true, ref reader); + + Assert.True(task.IsCompletedSuccessfully); + } + + [Fact] + public async Task InvokeAsync_CancelsAsyncTasksWhenCancellationTokenFires() + { + // Arrange + using var cts = new CancellationTokenSource(); + var runtime = new TestJSRuntime(); + + // Act + var task = runtime.InvokeAsync("test identifier 1", cts.Token, new object[] { "arg1", 123, true }); + + cts.Cancel(); + + // Assert + await Assert.ThrowsAsync(async () => await task); + } + + [Fact] + public async Task InvokeAsync_DoesNotStartWorkWhenCancellationHasBeenRequested() + { + // Arrange + using var cts = new CancellationTokenSource(); + cts.Cancel(); + var runtime = new TestJSRuntime(); + + // Act + var task = runtime.InvokeAsync("test identifier 1", cts.Token, new object[] { "arg1", 123, true }); + + cts.Cancel(); + + // Assert + await Assert.ThrowsAsync(async () => await task); + Assert.Empty(runtime.BeginInvokeCalls); + } + + [Fact] + public void CanCompleteAsyncCallsAsSuccess() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Act/Assert: Tasks not initially completed + var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); + var task = runtime.InvokeAsync("test identifier", Array.Empty()); + Assert.False(unrelatedTask.IsCompleted); + Assert.False(task.IsCompleted); + var bytes = Encoding.UTF8.GetBytes("\"my result\""); + var reader = new Utf8JsonReader(bytes); + + // Act/Assert: Task can be completed + runtime.EndInvokeJS( + runtime.BeginInvokeCalls[1].AsyncHandle, + /* succeeded: */ true, + ref reader); + Assert.False(unrelatedTask.IsCompleted); + Assert.True(task.IsCompleted); + Assert.Equal("my result", task.Result); + } + + [Fact] + public void CanCompleteAsyncCallsWithComplexType() + { + // Arrange + var runtime = new TestJSRuntime(); + + var task = runtime.InvokeAsync("test identifier", Array.Empty()); + var bytes = Encoding.UTF8.GetBytes("{\"id\":10, \"name\": \"Test\"}"); + var reader = new Utf8JsonReader(bytes); + + // Act/Assert: Task can be completed + runtime.EndInvokeJS( + runtime.BeginInvokeCalls[0].AsyncHandle, + /* succeeded: */ true, + ref reader); + Assert.True(task.IsCompleted); + var poco = task.Result; + Assert.Equal(10, poco.Id); + Assert.Equal("Test", poco.Name); + } + + [Fact] + public void CanCompleteAsyncCallsWithComplexTypeUsingPropertyCasing() + { + // Arrange + var runtime = new TestJSRuntime(); + + var task = runtime.InvokeAsync("test identifier", Array.Empty()); + var bytes = Encoding.UTF8.GetBytes("{\"Id\":10, \"Name\": \"Test\"}"); + var reader = new Utf8JsonReader(bytes); + reader.Read(); + + // Act/Assert: Task can be completed + runtime.EndInvokeJS( + runtime.BeginInvokeCalls[0].AsyncHandle, + /* succeeded: */ true, + ref reader); + Assert.True(task.IsCompleted); + var poco = task.Result; + Assert.Equal(10, poco.Id); + Assert.Equal("Test", poco.Name); + } + + [Fact] + public void CanCompleteAsyncCallsAsFailure() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Act/Assert: Tasks not initially completed + var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); + var task = runtime.InvokeAsync("test identifier", Array.Empty()); + Assert.False(unrelatedTask.IsCompleted); + Assert.False(task.IsCompleted); + var bytes = Encoding.UTF8.GetBytes("\"This is a test exception\""); + var reader = new Utf8JsonReader(bytes); + reader.Read(); + + // Act/Assert: Task can be failed + runtime.EndInvokeJS( + runtime.BeginInvokeCalls[1].AsyncHandle, + /* succeeded: */ false, + ref reader); + Assert.False(unrelatedTask.IsCompleted); + Assert.True(task.IsCompleted); + + var exception = Assert.IsType(task.AsTask().Exception); + var jsException = Assert.IsType(exception.InnerException); + Assert.Equal("This is a test exception", jsException.Message); + } + + [Fact] + public Task CanCompleteAsyncCallsWithErrorsDuringDeserialization() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Act/Assert: Tasks not initially completed + var unrelatedTask = runtime.InvokeAsync("unrelated call", Array.Empty()); + var task = runtime.InvokeAsync("test identifier", Array.Empty()); + Assert.False(unrelatedTask.IsCompleted); + Assert.False(task.IsCompleted); + var bytes = Encoding.UTF8.GetBytes("Not a string"); + var reader = new Utf8JsonReader(bytes); + + // Act/Assert: Task can be failed + runtime.EndInvokeJS( + runtime.BeginInvokeCalls[1].AsyncHandle, + /* succeeded: */ true, + ref reader); + Assert.False(unrelatedTask.IsCompleted); + + return AssertTask(); + + async Task AssertTask() + { + var jsException = await Assert.ThrowsAsync(async () => await task); + Assert.IsAssignableFrom(jsException.InnerException); + } + } + + [Fact] + public Task CompletingSameAsyncCallMoreThanOnce_IgnoresSecondResultAsync() + { + // Arrange + var runtime = new TestJSRuntime(); + + // Act/Assert + var task = runtime.InvokeAsync("test identifier", Array.Empty()); + var asyncHandle = runtime.BeginInvokeCalls[0].AsyncHandle; + var firstReader = new Utf8JsonReader(Encoding.UTF8.GetBytes("\"Some data\"")); + var secondReader = new Utf8JsonReader(Encoding.UTF8.GetBytes("\"Exception\"")); + + runtime.EndInvokeJS(asyncHandle, true, ref firstReader); + runtime.EndInvokeJS(asyncHandle, false, ref secondReader); + + return AssertTask(); + + async Task AssertTask() + { + var result = await task; + Assert.Equal("Some data", result); + } + } + + [Fact] + public void SerializesDotNetObjectWrappersInKnownFormat() + { + // Arrange + var runtime = new TestJSRuntime(); + JSRuntime.SetCurrentJSRuntime(runtime); + var obj1 = new object(); + var obj2 = new object(); + var obj3 = new object(); + + // Act + // Showing we can pass the DotNetObject either as top-level args or nested + var obj1Ref = DotNetObjectReference.Create(obj1); + var obj1DifferentRef = DotNetObjectReference.Create(obj1); + runtime.InvokeAsync("test identifier", + obj1Ref, + new Dictionary + { + { "obj2", DotNetObjectReference.Create(obj2) }, + { "obj3", DotNetObjectReference.Create(obj3) }, + { "obj1SameRef", obj1Ref }, + { "obj1DifferentRef", obj1DifferentRef }, + }); + + // 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: Objects were tracked + Assert.Same(obj1Ref, runtime.ObjectRefManager.FindDotNetObject(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); + } + + [Fact] + public void CanSanitizeDotNetInteropExceptions() + { + // Arrange + var expectedMessage = "An error ocurred while invoking '[Assembly]::Method'. Swapping to 'Development' environment will " + + "display more detailed information about the error that occurred."; + + string GetMessage(string assembly, string method) => $"An error ocurred while invoking '[{assembly}]::{method}'. Swapping to 'Development' environment will " + + "display more detailed information about the error that occurred."; + + var runtime = new TestJSRuntime() + { + OnDotNetException = (e, a, m) => new JSError { Message = GetMessage(a, m) } + }; + + var exception = new Exception("Some really sensitive data in here"); + + // Act + runtime.EndInvokeDotNet("0", false, exception, "Assembly", "Method", 0); + + // Assert + var call = runtime.EndInvokeDotNetCalls.Single(); + Assert.Equal("0", call.CallId); + Assert.False(call.Success); + var jsError = Assert.IsType(call.ResultOrError); + Assert.Equal(expectedMessage, jsError.Message); + } + + private class JSError + { + public string Message { get; set; } + } + + private class TestPoco + { + public int Id { get; set; } + + public string Name { get; set; } + } + + class TestJSRuntime : JSRuntime + { + public List BeginInvokeCalls = new List(); + public List EndInvokeDotNetCalls = new List(); + + public TimeSpan? DefaultTimeout + { + set + { + base.DefaultAsyncTimeout = value; + } + } + + public class BeginInvokeAsyncArgs + { + public long AsyncHandle { get; set; } + public string Identifier { get; set; } + public string ArgsJson { get; set; } + } + + public class EndInvokeDotNetArgs + { + public string CallId { get; set; } + public bool Success { get; set; } + public object ResultOrError { get; set; } + } + + public Func OnDotNetException { get; set; } + + protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) + { + if (OnDotNetException != null && !success) + { + resultOrError = OnDotNetException(resultOrError as Exception, assemblyName, methodIdentifier); + } + + EndInvokeDotNetCalls.Add(new EndInvokeDotNetArgs + { + CallId = callId, + Success = success, + ResultOrError = resultOrError + }); + } + + protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) + { + BeginInvokeCalls.Add(new BeginInvokeAsyncArgs + { + AsyncHandle = asyncHandle, + Identifier = identifier, + ArgsJson = argsJson, + }); + } } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs index c4e6b05c5b..48782fc4df 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Microsoft.JSInterop { - internal class TestJSRuntime : JSRuntimeBase + internal class TestJSRuntime : JSRuntime { protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { @@ -18,7 +18,7 @@ namespace Microsoft.JSInterop throw new NotImplementedException(); } - public static async Task WithJSRuntime(Action testCode) + 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 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 5299370576..2e4defd1b7 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 @@ -3,7 +3,7 @@ namespace Mono.WebAssembly.Interop { - public partial class MonoWebAssemblyJSRuntime : Microsoft.JSInterop.JSInProcessRuntimeBase + public partial class MonoWebAssemblyJSRuntime : Microsoft.JSInterop.JSInProcessRuntime { public MonoWebAssemblyJSRuntime() { } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { } diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs index e65df172f8..0e292a3e3c 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs @@ -5,6 +5,7 @@ using System; using System.Runtime.ExceptionServices; using System.Text.Json; using Microsoft.JSInterop; +using Microsoft.JSInterop.Infrastructure; using WebAssembly.JSInterop; namespace Mono.WebAssembly.Interop @@ -13,7 +14,7 @@ namespace Mono.WebAssembly.Interop /// Provides methods for invoking JavaScript functions for applications running /// on the Mono WebAssembly runtime. /// - public class MonoWebAssemblyJSRuntime : JSInProcessRuntimeBase + public class MonoWebAssemblyJSRuntime : JSInProcessRuntime { /// protected override string InvokeJS(string identifier, string argsJson) @@ -37,7 +38,7 @@ namespace Mono.WebAssembly.Interop // Invoked via Mono's JS interop mechanism (invoke_method) private static void EndInvokeJS(string argsJson) - => DotNetDispatcher.EndInvoke(argsJson); + => DotNetDispatcher.EndInvokeJS(argsJson); // Invoked via Mono's JS interop mechanism (invoke_method) private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) @@ -58,7 +59,7 @@ namespace Mono.WebAssembly.Interop assemblyName = assemblyNameOrDotNetObjectId; } - DotNetDispatcher.BeginInvoke(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); + DotNetDispatcher.BeginInvokeDotNet(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); } protected override void EndInvokeDotNet(