From edd5f54bc37af4f6ad444976dd7d6f5fe0ee0733 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 14 Aug 2019 09:44:33 -0700 Subject: [PATCH 1/6] Special case Disposing DotNetObjectReferences (dotnet/extensions#2176) * Special case Disposing DotNetObjectReferences This removes a public JSInvokable method required for disposing DotNetObjectReferences \n\nCommit migrated from https://github.com/dotnet/extensions/commit/d6bfc28e2104066dc363bf79bed39d6580977b26 --- .../src/src/Microsoft.JSInterop.ts | 11 ++-- .../ref/Microsoft.JSInterop.netcoreapp3.0.cs | 4 +- .../ref/Microsoft.JSInterop.netstandard2.0.cs | 4 +- .../src/DotNetDispatcher.cs | 45 +++++++-------- .../src/DotNetObjectRef.cs | 3 +- .../src/DotNetObjectRefManager.cs | 6 +- .../src/DotNetObjectRefOfT.cs | 56 ++++++++++++++++--- .../src/DotNetObjectReferenceJsonConverter.cs | 4 +- .../src/IDotNetObjectRef.cs | 1 + .../test/DotNetDispatcherTest.cs | 20 ++++++- .../test/DotNetObjectRefTest.cs | 3 +- .../test/JSInProcessRuntimeBaseTest.cs | 6 +- .../test/JSRuntimeBaseTest.cs | 8 +-- 13 files changed, 109 insertions(+), 62 deletions(-) diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index 60f6f800a6..30a91bde4d 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -66,7 +66,11 @@ module DotNet { } } - function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): Promise { + function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, ...args: any[]): Promise { + if (assemblyName && dotNetObjectId) { + throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ; + } + const asyncCallId = nextAsyncCallId++; const resultPromise = new Promise((resolve, reject) => { pendingAsyncCalls[asyncCallId] = { resolve, reject }; @@ -269,10 +273,7 @@ module DotNet { } public dispose() { - const promise = invokeMethodAsync( - 'Microsoft.JSInterop', - 'DotNetDispatcher.ReleaseDotNetObject', - this._id); + const promise = invokePossibleInstanceMethodAsync(null, '__Dispose', this._id); promise.catch(error => console.error(error)); } diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs index 654ae9d617..e73fa1be69 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs @@ -8,8 +8,6 @@ namespace Microsoft.JSInterop public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } public static void EndInvoke(string arguments) { } public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } - [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")] - public static void ReleaseDotNetObject(long dotNetObjectId) { } } public static partial class DotNetObjectRef { @@ -18,7 +16,7 @@ namespace Microsoft.JSInterop public sealed partial class DotNetObjectRef : System.IDisposable where TValue : class { internal DotNetObjectRef() { } - public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public TValue Value { get { throw null; } } public void Dispose() { } } public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime 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 654ae9d617..e73fa1be69 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -8,8 +8,6 @@ namespace Microsoft.JSInterop public static void BeginInvoke(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } public static void EndInvoke(string arguments) { } public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } - [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")] - public static void ReleaseDotNetObject(long dotNetObjectId) { } } public static partial class DotNetObjectRef { @@ -18,7 +16,7 @@ namespace Microsoft.JSInterop public sealed partial class DotNetObjectRef : System.IDisposable where TValue : class { internal DotNetObjectRef() { } - public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public TValue Value { get { throw null; } } public void Dispose() { } } public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs index 0dfac228a6..e639a33ff2 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs @@ -18,6 +18,7 @@ namespace Microsoft.JSInterop /// public static class DotNetDispatcher { + private const string DisposeDotNetObjectReferenceMethodName = "__Dispose"; internal static readonly JsonEncodedText DotNetObjectRefKey = JsonEncodedText.Encode("__dotNetObject"); private static readonly ConcurrentDictionary> _cachedMethodsByAssembly @@ -38,7 +39,7 @@ namespace Microsoft.JSInterop // the targeted method has [JSInvokable]. It is not itself subject to that restriction, // because there would be nobody to police that. This method *is* the police. - var targetInstance = (object)null; + IDotNetObjectRef targetInstance = default; if (dotNetObjectId != default) { targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); @@ -78,7 +79,7 @@ namespace Microsoft.JSInterop // original stack traces. object syncResult = null; ExceptionDispatchInfo syncException = null; - object targetInstance = null; + IDotNetObjectRef targetInstance = null; try { @@ -127,21 +128,28 @@ namespace Microsoft.JSInterop } } - private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, string argsJson) + private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectRef objectReference, string argsJson) { AssemblyKey assemblyKey; - if (targetInstance != null) + if (objectReference is null) + { + assemblyKey = new AssemblyKey(assemblyName); + } + else { if (assemblyName != null) { throw new ArgumentException($"For instance method calls, '{nameof(assemblyName)}' should be null. Value received: '{assemblyName}'."); } - assemblyKey = new AssemblyKey(targetInstance.GetType().Assembly); - } - else - { - assemblyKey = new AssemblyKey(assemblyName); + if (string.Equals(DisposeDotNetObjectReferenceMethodName, methodIdentifier, StringComparison.Ordinal)) + { + // The client executed dotNetObjectReference.dispose(). Dispose the reference and exit. + objectReference.Dispose(); + return default; + } + + assemblyKey = new AssemblyKey(objectReference.Value.GetType().Assembly); } var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier); @@ -150,7 +158,8 @@ namespace Microsoft.JSInterop try { - return methodInfo.Invoke(targetInstance, suppliedArgs); + // objectReference will be null if this call invokes a static JSInvokable method. + return methodInfo.Invoke(objectReference?.Value, suppliedArgs); } catch (TargetInvocationException tie) // Avoid using exception filters for AOT runtime support { @@ -280,22 +289,6 @@ namespace Microsoft.JSInterop } } - /// - /// Releases the reference to the specified .NET object. This allows the .NET runtime - /// to garbage collect that object if there are no other references to it. - /// - /// To avoid leaking memory, the JavaScript side code must call this for every .NET - /// object it obtains a reference to. The exception is if that object is used for - /// the entire lifetime of a given user's session, in which case it is released - /// automatically when the JavaScript runtime is disposed. - /// - /// The identifier previously passed to JavaScript code. - [JSInvokable(nameof(DotNetDispatcher) + "." + nameof(ReleaseDotNetObject))] - public static void ReleaseDotNetObject(long dotNetObjectId) - { - DotNetObjectRefManager.Current.ReleaseDotNetObject(dotNetObjectId); - } - private static (MethodInfo, Type[]) GetCachedMethodInfo(AssemblyKey assemblyKey, string methodIdentifier) { if (string.IsNullOrWhiteSpace(assemblyKey.AssemblyName)) diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs index af790281e9..f604bab272 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs @@ -15,8 +15,7 @@ namespace Microsoft.JSInterop /// An instance of . public static DotNetObjectRef Create(TValue value) where TValue : class { - var objectId = DotNetObjectRefManager.Current.TrackObject(value); - return new DotNetObjectRef(objectId, value); + return new DotNetObjectRef(DotNetObjectRefManager.Current, value); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs index ad1469e38f..a6be6aeb4f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs @@ -10,7 +10,7 @@ namespace Microsoft.JSInterop internal class DotNetObjectRefManager { private long _nextId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1 - private readonly ConcurrentDictionary _trackedRefsById = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _trackedRefsById = new ConcurrentDictionary(); public static DotNetObjectRefManager Current { @@ -25,7 +25,7 @@ namespace Microsoft.JSInterop } } - public long TrackObject(object dotNetObjectRef) + public long TrackObject(IDotNetObjectRef dotNetObjectRef) { var dotNetObjectId = Interlocked.Increment(ref _nextId); _trackedRefsById[dotNetObjectId] = dotNetObjectRef; @@ -33,7 +33,7 @@ namespace Microsoft.JSInterop return dotNetObjectId; } - public object FindDotNetObject(long dotNetObjectId) + public IDotNetObjectRef FindDotNetObject(long dotNetObjectId) { return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) ? dotNetObjectRef diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs index be6bf91663..d83d0e89bb 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs @@ -16,23 +16,53 @@ namespace Microsoft.JSInterop [JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))] public sealed class DotNetObjectRef : IDotNetObjectRef, IDisposable where TValue : class { + private readonly DotNetObjectRefManager _referenceManager; + private readonly TValue _value; + private readonly long _objectId; + /// /// Initializes a new instance of . /// - /// The object Id. + /// /// The value to pass by reference. - internal DotNetObjectRef(long objectId, TValue value) + internal DotNetObjectRef(DotNetObjectRefManager referenceManager, TValue value) { - ObjectId = objectId; - Value = value; + _referenceManager = referenceManager; + _objectId = _referenceManager.TrackObject(this); + _value = value; + } + + internal DotNetObjectRef(DotNetObjectRefManager referenceManager, long objectId, TValue value) + { + _referenceManager = referenceManager; + _objectId = objectId; + _value = value; } /// /// Gets the object instance represented by this wrapper. /// - public TValue Value { get; } + public TValue Value + { + get + { + ThrowIfDisposed(); + return _value; + } + } - internal long ObjectId { get; } + internal long ObjectId + { + get + { + ThrowIfDisposed(); + return _objectId; + } + } + + object IDotNetObjectRef.Value => Value; + + internal bool Disposed { get; private set; } /// /// Stops tracking this object reference, allowing it to be garbage collected @@ -41,7 +71,19 @@ namespace Microsoft.JSInterop /// public void Dispose() { - DotNetObjectRefManager.Current.ReleaseDotNetObject(ObjectId); + if (!Disposed) + { + Disposed = true; + _referenceManager.ReleaseDotNetObject(_objectId); + } + } + + private void ThrowIfDisposed() + { + if (Disposed) + { + throw new ObjectDisposedException(GetType().Name); + } } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs index eaabdbf9e6..71bfa28ad5 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs @@ -40,8 +40,8 @@ namespace Microsoft.JSInterop throw new JsonException($"Required property {DotNetObjectRefKey} not found."); } - var value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); - return new DotNetObjectRef(dotNetObjectId, value); + var referenceManager = DotNetObjectRefManager.Current; + return (DotNetObjectRef)referenceManager.FindDotNetObject(dotNetObjectId); } public override void Write(Utf8JsonWriter writer, DotNetObjectRef value, JsonSerializerOptions options) diff --git a/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs index b082d0ce10..da16fa60a0 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs @@ -7,5 +7,6 @@ namespace Microsoft.JSInterop { internal interface IDotNetObjectRef : IDisposable { + object Value { get; } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs index 282aa0f364..13e01c2304 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs @@ -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())); + var resultDto2 = Assert.IsType>(DotNetObjectRefManager.Current.FindDotNetObject(property.GetInt64())).Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(1299, resultDto2.IntVal); }); @@ -202,6 +202,20 @@ namespace Microsoft.JSInterop Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid); }); + [Fact] + public Task DotNetObjectReferencesCanBeDisposed() => WithJSRuntime(jsRuntime => + { + // Arrange + var targetInstance = new SomePublicType(); + var objectRef = DotNetObjectRef.Create(targetInstance); + + // Act + DotNetDispatcher.BeginInvoke(null, null, "__Dispose", objectRef.ObjectId, null); + + // Assert + Assert.True(objectRef.Disposed); + }); + [Fact] public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime => { @@ -230,7 +244,7 @@ namespace Microsoft.JSInterop var targetInstance = new SomePublicType(); var objectRef = DotNetObjectRef.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); - DotNetDispatcher.ReleaseDotNetObject(1); + objectRef.Dispose(); // Act/Assert var ex = Assert.Throws( @@ -320,7 +334,7 @@ namespace Microsoft.JSInterop // Assert Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson); - var resultDto = (TestDTO)jsRuntime.ObjectRefManager.FindDotNetObject(3); + var resultDto = ((DotNetObjectRef)jsRuntime.ObjectRefManager.FindDotNetObject(3)).Value; Assert.Equal(1235, resultDto.IntVal); Assert.Equal("MY STRING", resultDto.StringVal); }); diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs index 112363869e..22cb471f28 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs @@ -24,10 +24,11 @@ namespace Microsoft.JSInterop var objRef = DotNetObjectRef.Create(new object()); // Act + Assert.Equal(1, objRef.ObjectId); objRef.Dispose(); // Assert - var ex = Assert.Throws(() => jsRuntime.ObjectRefManager.FindDotNetObject(objRef.ObjectId)); + var ex = Assert.Throws(() => jsRuntime.ObjectRefManager.FindDotNetObject(1)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); }); } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs index a8d551e94e..d71969d450 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs @@ -60,9 +60,9 @@ namespace Microsoft.JSInterop.Tests Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3}}]", call.ArgsJson); // Assert: Objects were tracked - Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1)); - Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2)); - Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3)); + Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1).Value); + Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2).Value); + Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3).Value); } [Fact] diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs index 2714886f9a..c3bf4f9eef 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs @@ -282,10 +282,10 @@ namespace Microsoft.JSInterop Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":3},\"obj3\":{\"__dotNetObject\":4},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":2}}]", call.ArgsJson); // Assert: Objects were tracked - Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1)); - Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2)); - Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3)); - Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4)); + Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1).Value); + Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2).Value); + Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3).Value); + Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4).Value); } [Fact] From 9372816b7c436e06da6d6426e0863b3677aac83c Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 14 Aug 2019 12:24:35 -0700 Subject: [PATCH 2/6] 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( From c717230b1350ba5ad408de9155f2e784dcf3dbac Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 15 Aug 2019 09:12:53 -0700 Subject: [PATCH 3/6] Cleanup to skip/flaky attributes (dotnet/extensions#2186) \n\nCommit migrated from https://github.com/dotnet/extensions/commit/cfef5e07fb893b1c5a94566a0d053290f0c75382 --- src/Testing/src/TestPlatformHelper.cs | 2 +- .../src/xunit/ConditionalFactAttribute.cs | 2 +- .../src/xunit/ConditionalFactDiscoverer.cs | 2 +- .../src/xunit/ConditionalTheoryAttribute.cs | 2 +- .../src/xunit/ConditionalTheoryDiscoverer.cs | 2 +- src/Testing/src/xunit/DockerOnlyAttribute.cs | 2 +- ...vironmentVariableSkipConditionAttribute.cs | 2 +- src/Testing/src/xunit/FlakyAttribute.cs | 2 +- src/Testing/src/xunit/FlakyTestDiscoverer.cs | 2 +- .../xunit/FrameworkSkipConditionAttribute.cs | 2 +- src/Testing/src/xunit/IEnvironmentVariable.cs | 2 +- src/Testing/src/xunit/ITestCondition.cs | 2 +- .../src/xunit/MinimumOsVersionAttribute.cs | 2 +- .../src/xunit/OSSkipConditionAttribute.cs | 2 +- src/Testing/src/xunit/OperatingSystems.cs | 2 +- src/Testing/src/xunit/RuntimeFrameworks.cs | 2 +- src/Testing/src/xunit/SkipOnCIAttribute.cs | 43 ++++++++++++++++ src/Testing/src/xunit/SkipOnHelixAttribute.cs | 50 +++++++++++++++++++ src/Testing/src/xunit/SkippedTestCase.cs | 2 +- src/Testing/src/xunit/TestMethodExtensions.cs | 2 +- src/Testing/src/xunit/WindowsVersions.cs | 2 +- src/Testing/test/ConditionalFactTest.cs | 2 +- src/Testing/test/ConditionalTheoryTest.cs | 2 +- src/Testing/test/DockerTests.cs | 2 +- .../EnvironmentVariableSkipConditionTest.cs | 2 +- src/Testing/test/FlakyAttributeTest.cs | 4 +- .../test/OSSkipConditionAttributeTest.cs | 2 +- src/Testing/test/OSSkipConditionTest.cs | 2 +- src/Testing/test/SkipOnCITests.cs | 22 ++++++++ src/Testing/test/TestPlatformHelperTest.cs | 2 +- 30 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 src/Testing/src/xunit/SkipOnCIAttribute.cs create mode 100644 src/Testing/src/xunit/SkipOnHelixAttribute.cs create mode 100644 src/Testing/test/SkipOnCITests.cs diff --git a/src/Testing/src/TestPlatformHelper.cs b/src/Testing/src/TestPlatformHelper.cs index 1a3f275c7e..2c13e08eb3 100644 --- a/src/Testing/src/TestPlatformHelper.cs +++ b/src/Testing/src/TestPlatformHelper.cs @@ -20,4 +20,4 @@ namespace Microsoft.AspNetCore.Testing public static bool IsMac => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); } -} \ No newline at end of file +} diff --git a/src/Testing/src/xunit/ConditionalFactAttribute.cs b/src/Testing/src/xunit/ConditionalFactAttribute.cs index ce37df2e56..fdc108190a 100644 --- a/src/Testing/src/xunit/ConditionalFactAttribute.cs +++ b/src/Testing/src/xunit/ConditionalFactAttribute.cs @@ -5,7 +5,7 @@ using System; using Xunit; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] [XunitTestCaseDiscoverer("Microsoft.AspNetCore.Testing.xunit." + nameof(ConditionalFactDiscoverer), "Microsoft.AspNetCore.Testing")] diff --git a/src/Testing/src/xunit/ConditionalFactDiscoverer.cs b/src/Testing/src/xunit/ConditionalFactDiscoverer.cs index cf49b29e5a..ce190376fc 100644 --- a/src/Testing/src/xunit/ConditionalFactDiscoverer.cs +++ b/src/Testing/src/xunit/ConditionalFactDiscoverer.cs @@ -4,7 +4,7 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { internal class ConditionalFactDiscoverer : FactDiscoverer { diff --git a/src/Testing/src/xunit/ConditionalTheoryAttribute.cs b/src/Testing/src/xunit/ConditionalTheoryAttribute.cs index fe45f2ffc6..58b460e96e 100644 --- a/src/Testing/src/xunit/ConditionalTheoryAttribute.cs +++ b/src/Testing/src/xunit/ConditionalTheoryAttribute.cs @@ -5,7 +5,7 @@ using System; using Xunit; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] [XunitTestCaseDiscoverer("Microsoft.AspNetCore.Testing.xunit." + nameof(ConditionalTheoryDiscoverer), "Microsoft.AspNetCore.Testing")] diff --git a/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs b/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs index 9e413cd580..c9ee58889a 100644 --- a/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs +++ b/src/Testing/src/xunit/ConditionalTheoryDiscoverer.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Xunit.Abstractions; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { internal class ConditionalTheoryDiscoverer : TheoryDiscoverer { diff --git a/src/Testing/src/xunit/DockerOnlyAttribute.cs b/src/Testing/src/xunit/DockerOnlyAttribute.cs index d67a35a672..7d809884d6 100644 --- a/src/Testing/src/xunit/DockerOnlyAttribute.cs +++ b/src/Testing/src/xunit/DockerOnlyAttribute.cs @@ -6,7 +6,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] public sealed class DockerOnlyAttribute : Attribute, ITestCondition diff --git a/src/Testing/src/xunit/EnvironmentVariableSkipConditionAttribute.cs b/src/Testing/src/xunit/EnvironmentVariableSkipConditionAttribute.cs index fe215a8e0b..0599e31901 100644 --- a/src/Testing/src/xunit/EnvironmentVariableSkipConditionAttribute.cs +++ b/src/Testing/src/xunit/EnvironmentVariableSkipConditionAttribute.cs @@ -4,7 +4,7 @@ using System; using System.Linq; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { /// /// Skips a test when the value of an environment variable matches any of the supplied values. diff --git a/src/Testing/src/xunit/FlakyAttribute.cs b/src/Testing/src/xunit/FlakyAttribute.cs index ab4450e685..acea96f3cb 100644 --- a/src/Testing/src/xunit/FlakyAttribute.cs +++ b/src/Testing/src/xunit/FlakyAttribute.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { /// /// Marks a test as "Flaky" so that the build will sequester it and ignore failures. diff --git a/src/Testing/src/xunit/FlakyTestDiscoverer.cs b/src/Testing/src/xunit/FlakyTestDiscoverer.cs index 344b9b2378..aea2f9ea5b 100644 --- a/src/Testing/src/xunit/FlakyTestDiscoverer.cs +++ b/src/Testing/src/xunit/FlakyTestDiscoverer.cs @@ -4,7 +4,7 @@ using System.Linq; using Xunit.Abstractions; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { public class FlakyTestDiscoverer : ITraitDiscoverer { diff --git a/src/Testing/src/xunit/FrameworkSkipConditionAttribute.cs b/src/Testing/src/xunit/FrameworkSkipConditionAttribute.cs index 168076a434..b7719848a6 100644 --- a/src/Testing/src/xunit/FrameworkSkipConditionAttribute.cs +++ b/src/Testing/src/xunit/FrameworkSkipConditionAttribute.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class FrameworkSkipConditionAttribute : Attribute, ITestCondition diff --git a/src/Testing/src/xunit/IEnvironmentVariable.cs b/src/Testing/src/xunit/IEnvironmentVariable.cs index 068c210611..ed06ed6505 100644 --- a/src/Testing/src/xunit/IEnvironmentVariable.cs +++ b/src/Testing/src/xunit/IEnvironmentVariable.cs @@ -1,7 +1,7 @@ // 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. -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { internal interface IEnvironmentVariable { diff --git a/src/Testing/src/xunit/ITestCondition.cs b/src/Testing/src/xunit/ITestCondition.cs index bb6ff1f031..34767b8574 100644 --- a/src/Testing/src/xunit/ITestCondition.cs +++ b/src/Testing/src/xunit/ITestCondition.cs @@ -1,7 +1,7 @@ // 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. -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { public interface ITestCondition { diff --git a/src/Testing/src/xunit/MinimumOsVersionAttribute.cs b/src/Testing/src/xunit/MinimumOsVersionAttribute.cs index 89e3b19556..df4985d338 100644 --- a/src/Testing/src/xunit/MinimumOsVersionAttribute.cs +++ b/src/Testing/src/xunit/MinimumOsVersionAttribute.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; using Microsoft.Win32; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { /// /// Skips a test if the OS is the given type (Windows) and the OS version is less than specified. diff --git a/src/Testing/src/xunit/OSSkipConditionAttribute.cs b/src/Testing/src/xunit/OSSkipConditionAttribute.cs index 9996510718..7655a3b45a 100644 --- a/src/Testing/src/xunit/OSSkipConditionAttribute.cs +++ b/src/Testing/src/xunit/OSSkipConditionAttribute.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = true)] public class OSSkipConditionAttribute : Attribute, ITestCondition diff --git a/src/Testing/src/xunit/OperatingSystems.cs b/src/Testing/src/xunit/OperatingSystems.cs index c575d3e197..2ddacacab9 100644 --- a/src/Testing/src/xunit/OperatingSystems.cs +++ b/src/Testing/src/xunit/OperatingSystems.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { [Flags] public enum OperatingSystems diff --git a/src/Testing/src/xunit/RuntimeFrameworks.cs b/src/Testing/src/xunit/RuntimeFrameworks.cs index 2ec5ea7ec1..3a69022b88 100644 --- a/src/Testing/src/xunit/RuntimeFrameworks.cs +++ b/src/Testing/src/xunit/RuntimeFrameworks.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { [Flags] public enum RuntimeFrameworks diff --git a/src/Testing/src/xunit/SkipOnCIAttribute.cs b/src/Testing/src/xunit/SkipOnCIAttribute.cs new file mode 100644 index 0000000000..1ee0b8cde8 --- /dev/null +++ b/src/Testing/src/xunit/SkipOnCIAttribute.cs @@ -0,0 +1,43 @@ +// 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.AspNetCore.Testing +{ + /// + /// Skip test if running on CI + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class SkipOnCIAttribute : Attribute, ITestCondition + { + public SkipOnCIAttribute(string issueUrl = "") + { + IssueUrl = issueUrl; + } + + public string IssueUrl { get; } + + public bool IsMet + { + get + { + return !OnCI(); + } + } + + public string SkipReason + { + get + { + return $"This test is skipped on CI"; + } + } + + public static bool OnCI() => OnHelix() || OnAzdo(); + public static bool OnHelix() => !string.IsNullOrEmpty(GetTargetHelixQueue()); + public static string GetTargetHelixQueue() => Environment.GetEnvironmentVariable("helix"); + public static bool OnAzdo() => !string.IsNullOrEmpty(GetIfOnAzdo()); + public static string GetIfOnAzdo() => Environment.GetEnvironmentVariable("AGENT_OS"); + } +} diff --git a/src/Testing/src/xunit/SkipOnHelixAttribute.cs b/src/Testing/src/xunit/SkipOnHelixAttribute.cs new file mode 100644 index 0000000000..85e82c1154 --- /dev/null +++ b/src/Testing/src/xunit/SkipOnHelixAttribute.cs @@ -0,0 +1,50 @@ +// 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.Linq; + +namespace Microsoft.AspNetCore.Testing +{ + /// + /// Skip test if running on helix (or a particular helix queue). + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class SkipOnHelixAttribute : Attribute, ITestCondition + { + public SkipOnHelixAttribute(string issueUrl) + { + if (string.IsNullOrEmpty(issueUrl)) + { + throw new ArgumentException(); + } + IssueUrl = issueUrl; + } + + public string IssueUrl { get; } + + public bool IsMet + { + get + { + var skip = OnHelix() && (Queues == null || Queues.ToLowerInvariant().Split(';').Contains(GetTargetHelixQueue().ToLowerInvariant())); + return !skip; + } + } + + // Queues that should be skipped on, i.e. "Windows.10.Amd64.ClientRS4.VS2017.Open;OSX.1012.Amd64.Open" + public string Queues { get; set; } + + public string SkipReason + { + get + { + return $"This test is skipped on helix"; + } + } + + public static bool OnHelix() => !string.IsNullOrEmpty(GetTargetHelixQueue()); + + public static string GetTargetHelixQueue() => Environment.GetEnvironmentVariable("helix"); + } +} diff --git a/src/Testing/src/xunit/SkippedTestCase.cs b/src/Testing/src/xunit/SkippedTestCase.cs index 1c25c507b9..b514c57209 100644 --- a/src/Testing/src/xunit/SkippedTestCase.cs +++ b/src/Testing/src/xunit/SkippedTestCase.cs @@ -5,7 +5,7 @@ using System; using Xunit.Abstractions; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { public class SkippedTestCase : XunitTestCase { diff --git a/src/Testing/src/xunit/TestMethodExtensions.cs b/src/Testing/src/xunit/TestMethodExtensions.cs index 5ec3bb4ec3..96dd93eb7c 100644 --- a/src/Testing/src/xunit/TestMethodExtensions.cs +++ b/src/Testing/src/xunit/TestMethodExtensions.cs @@ -5,7 +5,7 @@ using System.Linq; using Xunit.Abstractions; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { public static class TestMethodExtensions { diff --git a/src/Testing/src/xunit/WindowsVersions.cs b/src/Testing/src/xunit/WindowsVersions.cs index ff8312b363..d89da44de3 100644 --- a/src/Testing/src/xunit/WindowsVersions.cs +++ b/src/Testing/src/xunit/WindowsVersions.cs @@ -1,7 +1,7 @@ // 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. -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { public static class WindowsVersions { diff --git a/src/Testing/test/ConditionalFactTest.cs b/src/Testing/test/ConditionalFactTest.cs index 9c5c6d037d..efc3a16dea 100644 --- a/src/Testing/test/ConditionalFactTest.cs +++ b/src/Testing/test/ConditionalFactTest.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.Testing; using Xunit; namespace Microsoft.AspNetCore.Testing diff --git a/src/Testing/test/ConditionalTheoryTest.cs b/src/Testing/test/ConditionalTheoryTest.cs index d824eb61b4..07cf6a968f 100644 --- a/src/Testing/test/ConditionalTheoryTest.cs +++ b/src/Testing/test/ConditionalTheoryTest.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.Testing; using Xunit; using Xunit.Abstractions; diff --git a/src/Testing/test/DockerTests.cs b/src/Testing/test/DockerTests.cs index c66fdd679c..12735057d3 100644 --- a/src/Testing/test/DockerTests.cs +++ b/src/Testing/test/DockerTests.cs @@ -3,7 +3,7 @@ using System; using System.Runtime.InteropServices; -using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.Testing; using Xunit; namespace Microsoft.AspNetCore.Testing diff --git a/src/Testing/test/EnvironmentVariableSkipConditionTest.cs b/src/Testing/test/EnvironmentVariableSkipConditionTest.cs index d5e7b6342b..cbc8e9adad 100644 --- a/src/Testing/test/EnvironmentVariableSkipConditionTest.cs +++ b/src/Testing/test/EnvironmentVariableSkipConditionTest.cs @@ -3,7 +3,7 @@ using Xunit; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { public class EnvironmentVariableSkipConditionTest { diff --git a/src/Testing/test/FlakyAttributeTest.cs b/src/Testing/test/FlakyAttributeTest.cs index 1b9a122d93..ae06e5cf50 100644 --- a/src/Testing/test/FlakyAttributeTest.cs +++ b/src/Testing/test/FlakyAttributeTest.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Testing.xunit; +// 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 Xunit; diff --git a/src/Testing/test/OSSkipConditionAttributeTest.cs b/src/Testing/test/OSSkipConditionAttributeTest.cs index 0120eb7a4c..199af3ab6e 100644 --- a/src/Testing/test/OSSkipConditionAttributeTest.cs +++ b/src/Testing/test/OSSkipConditionAttributeTest.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; using Xunit; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { public class OSSkipConditionAttributeTest { diff --git a/src/Testing/test/OSSkipConditionTest.cs b/src/Testing/test/OSSkipConditionTest.cs index 2d76f2c2cd..a7904b1730 100644 --- a/src/Testing/test/OSSkipConditionTest.cs +++ b/src/Testing/test/OSSkipConditionTest.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; using Xunit; -namespace Microsoft.AspNetCore.Testing.xunit +namespace Microsoft.AspNetCore.Testing { public class OSSkipConditionTest { diff --git a/src/Testing/test/SkipOnCITests.cs b/src/Testing/test/SkipOnCITests.cs new file mode 100644 index 0000000000..8df5e73c30 --- /dev/null +++ b/src/Testing/test/SkipOnCITests.cs @@ -0,0 +1,22 @@ +// 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 Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Testing.Tests +{ + public class SkipOnCITests + { + [ConditionalFact] + [SkipOnCI] + public void AlwaysSkipOnCI() + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("HELIX")) || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AGENT_OS"))) + { + throw new Exception("Flaky!"); + } + } + } +} diff --git a/src/Testing/test/TestPlatformHelperTest.cs b/src/Testing/test/TestPlatformHelperTest.cs index 8e35e164d5..b1c2fbf2f8 100644 --- a/src/Testing/test/TestPlatformHelperTest.cs +++ b/src/Testing/test/TestPlatformHelperTest.cs @@ -1,7 +1,7 @@ // 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.AspNetCore.Testing.xunit; +using Microsoft.AspNetCore.Testing; using Xunit; namespace Microsoft.AspNetCore.Testing From ef83e3359d0a40f78c0e3e65d884db64c76cb5cb Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 15 Aug 2019 17:14:03 -0700 Subject: [PATCH 4/6] Change JSInterop to avoid using async locals (dotnet/extensions#2163) * Remove the use of async local JSRuntime * Update DotNetDispatcher to accept a JSRuntime instance rather than use a ambient value. * Modify DotNetObjectReference to start tracking it's value during serialization. \n\nCommit migrated from https://github.com/dotnet/extensions/commit/ae9878bb9945423ad20f0ba97033fcebfb5d8419 --- .../src/src/Microsoft.JSInterop.ts | 6 +- .../ref/Microsoft.JSInterop.netcoreapp3.0.cs | 8 +- .../ref/Microsoft.JSInterop.netstandard2.0.cs | 8 +- .../src/DotNetObjectReference.cs | 4 +- .../src/DotNetObjectReferenceOfT.cs | 42 ++- .../src/Infrastructure/DotNetDispatcher.cs | 49 ++-- .../DotNetObjectReferenceJsonConverter.cs | 15 +- ...tNetObjectReferenceJsonConverterFactory.cs | 9 +- .../DotNetObjectReferenceManager.cs | 51 ---- .../src/JSInProcessRuntime.cs | 4 +- .../Microsoft.JSInterop/src/JSRuntime.cs | 92 +++++-- .../src/JsonSerializerOptionsProvider.cs | 17 -- .../test/DotNetObjectReferenceTest.cs | 84 +++++- .../Infrastructure/DotNetDispatcherTest.cs | 255 +++++++++--------- .../DotNetObjectReferenceJsonConverterTest.cs | 72 ++--- .../test/JSInProcessRuntimeTest.cs | 9 +- .../Microsoft.JSInterop/test/JSRuntimeTest.cs | 30 +-- .../Microsoft.JSInterop/test/TestJSRuntime.cs | 12 - ...Mono.WebAssembly.Interop.netstandard2.0.cs | 1 + .../src/MonoWebAssemblyJSRuntime.cs | 27 +- 20 files changed, 432 insertions(+), 363 deletions(-) delete mode 100644 src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs delete mode 100644 src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs diff --git a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts index 30a91bde4d..3af355f6d0 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -55,7 +55,7 @@ module DotNet { return invokePossibleInstanceMethodAsync(assemblyName, methodIdentifier, null, args); } - function invokePossibleInstanceMethod(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[]): T { + function invokePossibleInstanceMethod(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): T { const dispatcher = getRequiredDispatcher(); if (dispatcher.invokeDotNetFromJS) { const argsJson = JSON.stringify(args, argReplacer); @@ -66,7 +66,7 @@ module DotNet { } } - function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, ...args: any[]): Promise { + function invokePossibleInstanceMethodAsync(assemblyName: string | null, methodIdentifier: string, dotNetObjectId: number | null, args: any[] | null): Promise { if (assemblyName && dotNetObjectId) { throw new Error(`For instance method calls, assemblyName should be null. Received '${assemblyName}'.`) ; } @@ -273,7 +273,7 @@ module DotNet { } public dispose() { - const promise = invokePossibleInstanceMethodAsync(null, '__Dispose', this._id); + const promise = invokePossibleInstanceMethodAsync(null, '__Dispose', this._id, null); promise.catch(error => console.error(error)); } diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs index a5fbbc768a..953f8b0329 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs @@ -48,11 +48,11 @@ namespace Microsoft.JSInterop { protected JSRuntime() { } protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId); public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, object[] args) { throw null; } public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; } - public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } } public static partial class JSRuntimeExtensions { @@ -72,8 +72,8 @@ namespace Microsoft.JSInterop.Infrastructure { public static partial class DotNetDispatcher { - public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } - public static void EndInvokeJS(string arguments) { } - public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } } } diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs index a5fbbc768a..953f8b0329 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -48,11 +48,11 @@ namespace Microsoft.JSInterop { protected JSRuntime() { } protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId); public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, object[] args) { throw null; } public System.Threading.Tasks.ValueTask InvokeAsync(string identifier, System.Threading.CancellationToken cancellationToken, object[] args) { throw null; } - public static void SetCurrentJSRuntime(Microsoft.JSInterop.IJSRuntime instance) { } } public static partial class JSRuntimeExtensions { @@ -72,8 +72,8 @@ namespace Microsoft.JSInterop.Infrastructure { public static partial class DotNetDispatcher { - public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } - public static void EndInvokeJS(string arguments) { } - public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs index 24b13f0c85..989d8062bb 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.JSInterop.Infrastructure; - namespace Microsoft.JSInterop { /// @@ -17,7 +15,7 @@ namespace Microsoft.JSInterop /// An instance of . public static DotNetObjectReference Create(TValue value) where TValue : class { - return new DotNetObjectReference(DotNetObjectReferenceManager.Current, value); + return new DotNetObjectReference(value); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs index eb1ac6a234..773c2ed9a3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs @@ -2,7 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text.Json.Serialization; +using System.Diagnostics; using Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop @@ -14,22 +14,18 @@ namespace Microsoft.JSInterop /// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code. /// /// The type of the value to wrap. - [JsonConverter(typeof(DotNetObjectReferenceJsonConverterFactory))] public sealed class DotNetObjectReference : IDotNetObjectReference, IDisposable where TValue : class { - private readonly DotNetObjectReferenceManager _referenceManager; private readonly TValue _value; - private readonly long _objectId; + private long _objectId; + private JSRuntime _jsRuntime; /// /// Initializes a new instance of . /// - /// /// The value to pass by reference. - internal DotNetObjectReference(DotNetObjectReferenceManager referenceManager, TValue value) + internal DotNetObjectReference(TValue value) { - _referenceManager = referenceManager; - _objectId = _referenceManager.TrackObject(this); _value = value; } @@ -50,8 +46,30 @@ namespace Microsoft.JSInterop get { ThrowIfDisposed(); + Debug.Assert(_objectId != 0, "Accessing ObjectId without tracking is always incorrect."); + return _objectId; } + set + { + ThrowIfDisposed(); + _objectId = value; + } + } + + internal JSRuntime JSRuntime + { + get + { + ThrowIfDisposed(); + return _jsRuntime; + } + set + { + ThrowIfDisposed(); + _jsRuntime = value; + } + } object IDotNetObjectReference.Value => Value; @@ -68,11 +86,15 @@ namespace Microsoft.JSInterop if (!Disposed) { Disposed = true; - _referenceManager.ReleaseDotNetObject(_objectId); + + if (_jsRuntime != null) + { + _jsRuntime.ReleaseObjectReference(_objectId); + } } } - private void ThrowIfDisposed() + internal void ThrowIfDisposed() { if (Disposed) { diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index d4a4de14dd..92afc6278d 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -27,12 +27,13 @@ namespace Microsoft.JSInterop.Infrastructure /// /// Receives a call from JS to .NET, locating and invoking the specified method. /// + /// The . /// The assembly containing the method to be invoked. /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. /// For instance method calls, identifies the target object. /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static string Invoke(string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static string Invoke(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { // This method doesn't need [JSInvokable] because the platform is responsible for having // some way to dispatch calls here. The logic inside here is the thing that checks whether @@ -42,41 +43,38 @@ namespace Microsoft.JSInterop.Infrastructure IDotNetObjectReference targetInstance = default; if (dotNetObjectId != default) { - targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(dotNetObjectId); } - var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + var syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson); if (syncResult == null) { return null; } - return JsonSerializer.Serialize(syncResult, JsonSerializerOptionsProvider.Options); + return JsonSerializer.Serialize(syncResult, jsRuntime.JsonSerializerOptions); } /// /// Receives a call from JS to .NET, locating and invoking the specified method asynchronously. /// + /// The . /// A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required. /// The assembly containing the method to be invoked. /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. /// For instance method calls, identifies the target object. /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static void BeginInvokeDotNet(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static void BeginInvokeDotNet(JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { // This method doesn't need [JSInvokable] because the platform is responsible for having // some way to dispatch calls here. The logic inside here is the thing that checks whether // the targeted method has [JSInvokable]. It is not itself subject to that restriction, // because there would be nobody to police that. This method *is* the police. - // DotNetDispatcher only works with JSRuntimeBase instances. - // If the developer wants to use a totally custom IJSRuntime, then their JS-side - // code has to implement its own way of returning async results. - var jsRuntimeBaseInstance = (JSRuntime)JSRuntime.Current; - // Using ExceptionDispatchInfo here throughout because we want to always preserve // original stack traces. + object syncResult = null; ExceptionDispatchInfo syncException = null; IDotNetObjectReference targetInstance = null; @@ -85,10 +83,10 @@ namespace Microsoft.JSInterop.Infrastructure { if (dotNetObjectId != default) { - targetInstance = DotNetObjectReferenceManager.Current.FindDotNetObject(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(dotNetObjectId); } - syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson); } catch (Exception ex) { @@ -103,7 +101,7 @@ namespace Microsoft.JSInterop.Infrastructure else if (syncException != null) { // Threw synchronously, let's respond. - jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId); } else if (syncResult is Task task) { @@ -115,20 +113,20 @@ namespace Microsoft.JSInterop.Infrastructure { var exception = t.Exception.GetBaseException(); - jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId); } var result = TaskGenericsUtil.GetTaskResult(task); - jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId); }, TaskScheduler.Current); } else { - jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId); } } - private static object InvokeSynchronously(string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson) + private static object InvokeSynchronously(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson) { AssemblyKey assemblyKey; if (objectReference is null) @@ -154,7 +152,7 @@ namespace Microsoft.JSInterop.Infrastructure var (methodInfo, parameterTypes) = GetCachedMethodInfo(assemblyKey, methodIdentifier); - var suppliedArgs = ParseArguments(methodIdentifier, argsJson, parameterTypes); + var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes); try { @@ -173,7 +171,7 @@ namespace Microsoft.JSInterop.Infrastructure } } - internal static object[] ParseArguments(string methodIdentifier, string arguments, Type[] parameterTypes) + internal static object[] ParseArguments(JSRuntime jsRuntime, string methodIdentifier, string arguments, Type[] parameterTypes) { if (parameterTypes.Length == 0) { @@ -198,7 +196,7 @@ namespace Microsoft.JSInterop.Infrastructure throw new InvalidOperationException($"In call to '{methodIdentifier}', parameter of type '{parameterType.Name}' at index {(index + 1)} must be declared as type 'DotNetObjectRef<{parameterType.Name}>' to receive the incoming value."); } - suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, JsonSerializerOptionsProvider.Options); + suppliedArgs[index] = JsonSerializer.Deserialize(ref reader, parameterType, jsRuntime.JsonSerializerOptions); index++; } @@ -247,18 +245,13 @@ namespace Microsoft.JSInterop.Infrastructure /// method is responsible for handling any possible exception generated from the arguments /// passed in as parameters. /// + /// The . /// The serialized arguments for the callback completion. /// /// This method can throw any exception either from the argument received or as a result /// of executing any callback synchronously upon completion. /// - public static void EndInvokeJS(string arguments) - { - var jsRuntimeBase = (JSRuntime)JSRuntime.Current; - ParseEndInvokeArguments(jsRuntimeBase, arguments); - } - - internal static void ParseEndInvokeArguments(JSRuntime jsRuntimeBase, string arguments) + public static void EndInvokeJS(JSRuntime jsRuntime, string arguments) { var utf8JsonBytes = Encoding.UTF8.GetBytes(arguments); @@ -281,7 +274,7 @@ namespace Microsoft.JSInterop.Infrastructure var success = reader.GetBoolean(); reader.Read(); - jsRuntimeBase.EndInvokeJS(taskId, success, ref reader); + jsRuntime.EndInvokeJS(taskId, success, ref reader); if (!reader.Read() || reader.TokenType != JsonTokenType.EndArray) { diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs index c077ac0b17..7658bbc2c3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs @@ -9,8 +9,15 @@ namespace Microsoft.JSInterop.Infrastructure { internal sealed class DotNetObjectReferenceJsonConverter : JsonConverter> where TValue : class { + public DotNetObjectReferenceJsonConverter(JSRuntime jsRuntime) + { + JSRuntime = jsRuntime; + } + private static JsonEncodedText DotNetObjectRefKey => DotNetDispatcher.DotNetObjectRefKey; + public JSRuntime JSRuntime { get; } + public override DotNetObjectReference Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { long dotNetObjectId = 0; @@ -40,14 +47,16 @@ namespace Microsoft.JSInterop.Infrastructure throw new JsonException($"Required property {DotNetObjectRefKey} not found."); } - var referenceManager = DotNetObjectReferenceManager.Current; - return (DotNetObjectReference)referenceManager.FindDotNetObject(dotNetObjectId); + var value = (DotNetObjectReference)JSRuntime.GetObjectReference(dotNetObjectId); + return value; } public override void Write(Utf8JsonWriter writer, DotNetObjectReference value, JsonSerializerOptions options) { + var objectId = JSRuntime.TrackObjectReference(value); + writer.WriteStartObject(); - writer.WriteNumber(DotNetObjectRefKey, value.ObjectId); + writer.WriteNumber(DotNetObjectRefKey, objectId); writer.WriteEndObject(); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs index 350530b624..288bfdd090 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs @@ -9,6 +9,13 @@ namespace Microsoft.JSInterop.Infrastructure { internal sealed class DotNetObjectReferenceJsonConverterFactory : JsonConverterFactory { + public DotNetObjectReferenceJsonConverterFactory(JSRuntime jsRuntime) + { + JSRuntime = jsRuntime; + } + + public JSRuntime JSRuntime { get; } + public override bool CanConvert(Type typeToConvert) { return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectReference<>); @@ -20,7 +27,7 @@ namespace Microsoft.JSInterop.Infrastructure var instanceType = typeToConvert.GetGenericArguments()[0]; var converterType = typeof(DotNetObjectReferenceJsonConverter<>).MakeGenericType(instanceType); - return (JsonConverter)Activator.CreateInstance(converterType); + return (JsonConverter)Activator.CreateInstance(converterType, JSRuntime); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs deleted file mode 100644 index 709dd963fa..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceManager.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Concurrent; -using System.Threading; - -namespace Microsoft.JSInterop.Infrastructure -{ - internal class DotNetObjectReferenceManager - { - private long _nextId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1 - private readonly ConcurrentDictionary _trackedRefsById = new ConcurrentDictionary(); - - public static DotNetObjectReferenceManager Current - { - get - { - if (!(JSRuntime.Current is JSRuntime jsRuntime)) - { - throw new InvalidOperationException("JSRuntime must be set up correctly and must be an instance of JSRuntimeBase to use DotNetObjectReference."); - } - - return jsRuntime.ObjectRefManager; - } - } - - public long TrackObject(IDotNetObjectReference dotNetObjectRef) - { - var dotNetObjectId = Interlocked.Increment(ref _nextId); - _trackedRefsById[dotNetObjectId] = dotNetObjectRef; - - return dotNetObjectId; - } - - public IDotNetObjectReference FindDotNetObject(long dotNetObjectId) - { - return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) - ? dotNetObjectRef - : throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectRef instance was already disposed.", nameof(dotNetObjectId)); - - } - - /// - /// Stops tracking the specified .NET object reference. - /// This may be invoked either by disposing a DotNetObjectRef in .NET code, or via JS interop by calling "dispose" on the corresponding instance in JavaScript code - /// - /// The ID of the . - public void ReleaseDotNetObject(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _); - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs index cf8cc7030b..2b96bbbbb5 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs @@ -19,13 +19,13 @@ namespace Microsoft.JSInterop /// An instance of obtained by JSON-deserializing the return value. public TValue Invoke(string identifier, params object[] args) { - var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options)); + var resultJson = InvokeJS(identifier, JsonSerializer.Serialize(args, JsonSerializerOptions)); if (resultJson is null) { return default; } - return JsonSerializer.Deserialize(resultJson, JsonSerializerOptionsProvider.Options); + return JsonSerializer.Deserialize(resultJson, JsonSerializerOptions); } /// diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 598b47c4d4..ba411b72db 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Linq; using System.Text.Json; using System.Threading; @@ -16,36 +17,40 @@ namespace Microsoft.JSInterop /// public abstract partial class JSRuntime : IJSRuntime { - private static readonly AsyncLocal _currentJSRuntime = new AsyncLocal(); - - internal static IJSRuntime Current => _currentJSRuntime.Value; - + private long _nextObjectReferenceId = 0; // 0 signals no object, but we increment prior to assignment. The first tracked object should have id 1 private long _nextPendingTaskId = 1; // Start at 1 because zero signals "no response needed" - private readonly ConcurrentDictionary _pendingTasks - = new ConcurrentDictionary(); - + private readonly ConcurrentDictionary _pendingTasks = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _trackedRefsById = new ConcurrentDictionary(); private readonly ConcurrentDictionary _cancellationRegistrations = new ConcurrentDictionary(); - internal DotNetObjectReferenceManager ObjectRefManager { get; } = new DotNetObjectReferenceManager(); + /// + /// Initializes a new instance of . + /// + protected JSRuntime() + { + JsonSerializerOptions = new JsonSerializerOptions + { + MaxDepth = 32, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + Converters = + { + new DotNetObjectReferenceJsonConverterFactory(this), + } + }; + } + + /// + /// Gets the used to serialize and deserialize interop payloads. + /// + protected internal JsonSerializerOptions JsonSerializerOptions { get; } /// /// Gets or sets the default timeout for asynchronous JavaScript calls. /// protected TimeSpan? DefaultAsyncTimeout { get; set; } - /// - /// Sets the current JS runtime to the supplied instance. - /// - /// This is intended for framework use. Developers should not normally need to call this method. - /// - /// The new current . - public static void SetCurrentJSRuntime(IJSRuntime instance) - { - _currentJSRuntime.Value = instance - ?? throw new ArgumentNullException(nameof(instance)); - } - /// /// Invokes the specified JavaScript function asynchronously. /// @@ -103,7 +108,7 @@ namespace Microsoft.JSInterop } var argsJson = args?.Any() == true ? - JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options) : + JsonSerializer.Serialize(args, JsonSerializerOptions) : null; BeginInvokeJS(taskId, identifier, argsJson); @@ -176,7 +181,7 @@ namespace Microsoft.JSInterop { var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs); - var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptionsProvider.Options); + var result = JsonSerializer.Deserialize(ref jsonReader, resultType, JsonSerializerOptions); TaskGenericsUtil.SetTaskCompletionSourceResult(tcs, result); } else @@ -191,5 +196,48 @@ namespace Microsoft.JSInterop TaskGenericsUtil.SetTaskCompletionSourceException(tcs, new JSException(message, exception)); } } + + internal long TrackObjectReference(DotNetObjectReference dotNetObjectReference) where TValue : class + { + if (dotNetObjectReference == null) + { + throw new ArgumentNullException(nameof(dotNetObjectReference)); + } + + dotNetObjectReference.ThrowIfDisposed(); + + var jsRuntime = dotNetObjectReference.JSRuntime; + if (jsRuntime is null) + { + var dotNetObjectId = Interlocked.Increment(ref _nextObjectReferenceId); + + dotNetObjectReference.JSRuntime = this; + dotNetObjectReference.ObjectId = dotNetObjectId; + + _trackedRefsById[dotNetObjectId] = dotNetObjectReference; + } + else if (!ReferenceEquals(this, jsRuntime)) + { + throw new InvalidOperationException($"{dotNetObjectReference.GetType().Name} is already being tracked by a different instance of {nameof(JSRuntime)}." + + $" A common cause is caching an instance of {nameof(DotNetObjectReference)} globally. Consider creating instances of {nameof(DotNetObjectReference)} at the JSInterop callsite."); + } + + Debug.Assert(dotNetObjectReference.ObjectId != 0); + return dotNetObjectReference.ObjectId; + } + + internal IDotNetObjectReference GetObjectReference(long dotNetObjectId) + { + return _trackedRefsById.TryGetValue(dotNetObjectId, out var dotNetObjectRef) + ? dotNetObjectRef + : throw new ArgumentException($"There is no tracked object with id '{dotNetObjectId}'. Perhaps the DotNetObjectReference instance was already disposed.", nameof(dotNetObjectId)); + } + + /// + /// Stops tracking the specified .NET object reference. + /// This may be invoked either by disposing a DotNetObjectRef in .NET code, or via JS interop by calling "dispose" on the corresponding instance in JavaScript code + /// + /// The ID of the . + internal void ReleaseObjectReference(long dotNetObjectId) => _trackedRefsById.TryRemove(dotNetObjectId, out _); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs b/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs deleted file mode 100644 index 62244270e3..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Text.Json; - -namespace Microsoft.JSInterop -{ - internal static class JsonSerializerOptionsProvider - { - public static readonly JsonSerializerOptions Options = new JsonSerializerOptions - { - MaxDepth = 32, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - }; - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs index bcd5c95028..95fad485a7 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs @@ -2,34 +2,102 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Threading.Tasks; using Xunit; -using static Microsoft.JSInterop.TestJSRuntime; namespace Microsoft.JSInterop { public class DotNetObjectReferenceTest { [Fact] - public Task CanAccessValue() => WithJSRuntime(_ => + public void CanAccessValue() { var obj = new object(); Assert.Same(obj, DotNetObjectReference.Create(obj).Value); - }); + } [Fact] - public Task NotifiesAssociatedJsRuntimeOfDisposal() => WithJSRuntime(jsRuntime => + public void TrackObjectReference_AssignsObjectId() { // Arrange + var jsRuntime = new TestJSRuntime(); var objRef = DotNetObjectReference.Create(new object()); // Act + var objectId = jsRuntime.TrackObjectReference(objRef); + + // Act + Assert.Equal(objectId, objRef.ObjectId); Assert.Equal(1, objRef.ObjectId); + } + + [Fact] + public void TrackObjectReference_AllowsMultipleCallsUsingTheSameJSRuntime() + { + // Arrange + var jsRuntime = new TestJSRuntime(); + var objRef = DotNetObjectReference.Create(new object()); + + // Act + var objectId1 = jsRuntime.TrackObjectReference(objRef); + var objectId2 = jsRuntime.TrackObjectReference(objRef); + + // Act + Assert.Equal(objectId1, objectId2); + } + + [Fact] + public void TrackObjectReference_ThrowsIfDifferentJSRuntimeInstancesAreUsed() + { + // Arrange + var objRef = DotNetObjectReference.Create("Hello world"); + var expected = $"{objRef.GetType().Name} is already being tracked by a different instance of {nameof(JSRuntime)}. A common cause is caching an instance of {nameof(DotNetObjectReference)}" + + $" globally. Consider creating instances of {nameof(DotNetObjectReference)} at the JSInterop callsite."; + var jsRuntime1 = new TestJSRuntime(); + var jsRuntime2 = new TestJSRuntime(); + jsRuntime1.TrackObjectReference(objRef); + + // Act + var ex = Assert.Throws(() => jsRuntime2.TrackObjectReference(objRef)); + + // Assert + Assert.Equal(expected, ex.Message); + } + + [Fact] + public void Dispose_StopsTrackingObject() + { + // Arrange + var objRef = DotNetObjectReference.Create("Hello world"); + var jsRuntime = new TestJSRuntime(); + jsRuntime.TrackObjectReference(objRef); + var objectId = objRef.ObjectId; + var expected = $"There is no tracked object with id '{objectId}'. Perhaps the DotNetObjectReference instance was already disposed."; + + // Act + Assert.Same(objRef, jsRuntime.GetObjectReference(objectId)); objRef.Dispose(); // Assert - var ex = Assert.Throws(() => jsRuntime.ObjectRefManager.FindDotNetObject(1)); - Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); - }); + Assert.True(objRef.Disposed); + Assert.Throws(() => jsRuntime.GetObjectReference(objectId)); + } + + [Fact] + public void DoubleDispose_Works() + { + // Arrange + var objRef = DotNetObjectReference.Create("Hello world"); + var jsRuntime = new TestJSRuntime(); + jsRuntime.TrackObjectReference(objRef); + var objectId = objRef.ObjectId; + + // Act + Assert.Same(objRef, jsRuntime.GetObjectReference(objectId)); + objRef.Dispose(); + + // Assert + objRef.Dispose(); + // If we got this far, this did not throw. + } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index d9ddac2a89..2d8208b7a6 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs @@ -20,7 +20,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(" ", "SomeMethod", default, "[]"); + DotNetDispatcher.Invoke(new TestJSRuntime(), " ", "SomeMethod", default, "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -32,7 +32,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke("SomeAssembly", " ", default, "[]"); + DotNetDispatcher.Invoke(new TestJSRuntime(), "SomeAssembly", " ", default, "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -45,7 +45,7 @@ namespace Microsoft.JSInterop.Infrastructure var assemblyName = "Some.Fake.Assembly"; var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(assemblyName, "SomeMethod", default, null); + DotNetDispatcher.Invoke(new TestJSRuntime(), assemblyName, "SomeMethod", default, null); }); Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message); @@ -67,52 +67,56 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(thisAssemblyName, methodIdentifier, default, null); + DotNetDispatcher.Invoke(new TestJSRuntime(), thisAssemblyName, methodIdentifier, default, null); }); Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message); } [Fact] - public Task CanInvokeStaticVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeStaticVoidMethod() { // Arrange/Act + var jsRuntime = new TestJSRuntime(); SomePublicType.DidInvokeMyInvocableStaticVoid = false; - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticVoid", default, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticVoid", default, null); // Assert Assert.Null(resultJson); Assert.True(SomePublicType.DidInvokeMyInvocableStaticVoid); - }); + } [Fact] - public Task CanInvokeStaticNonVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeStaticNonVoidMethod() { // Arrange/Act - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticNonVoid", default, null); - var result = JsonSerializer.Deserialize(resultJson, JsonSerializerOptionsProvider.Options); + var jsRuntime = new TestJSRuntime(); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticNonVoid", default, null); + var result = JsonSerializer.Deserialize(resultJson, jsRuntime.JsonSerializerOptions); // Assert Assert.Equal("Test", result.StringVal); Assert.Equal(123, result.IntVal); - }); + } [Fact] - public Task CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() => WithJSRuntime(jsRuntime => + public void CanInvokeStaticNonVoidMethodWithoutCustomIdentifier() { // Arrange/Act - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); - var result = JsonSerializer.Deserialize(resultJson, JsonSerializerOptionsProvider.Options); + var jsRuntime = new TestJSRuntime(); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); + var result = JsonSerializer.Deserialize(resultJson, jsRuntime.JsonSerializerOptions); // Assert Assert.Equal("InvokableMethodWithoutCustomIdentifier", result.StringVal); Assert.Equal(456, result.IntVal); - }); + } [Fact] - public Task CanInvokeStaticWithParams() => WithJSRuntime(jsRuntime => + public void CanInvokeStaticWithParams() { // Arrange: Track a .NET object to use as an arg + var jsRuntime = new TestJSRuntime(); var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; var objectRef = DotNetObjectReference.Create(arg3); jsRuntime.Invoke("unimportant", objectRef); @@ -123,15 +127,15 @@ namespace Microsoft.JSInterop.Infrastructure new TestDTO { StringVal = "Another string", IntVal = 456 }, new[] { 100, 200 }, objectRef - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act - var resultJson = DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); var result = JsonDocument.Parse(resultJson); var root = result.RootElement; // Assert: First result value marshalled via JSON - var resultDto1 = JsonSerializer.Deserialize(root[0].GetRawText(), JsonSerializerOptionsProvider.Options); + var resultDto1 = JsonSerializer.Deserialize(root[0].GetRawText(), jsRuntime.JsonSerializerOptions); Assert.Equal("ANOTHER STRING", resultDto1.StringVal); Assert.Equal(756, resultDto1.IntVal); @@ -142,15 +146,16 @@ namespace Microsoft.JSInterop.Infrastructure Assert.False(resultDto2Ref.TryGetProperty(nameof(TestDTO.IntVal), out _)); Assert.True(resultDto2Ref.TryGetProperty(DotNetDispatcher.DotNetObjectRefKey.EncodedUtf8Bytes, out var property)); - var resultDto2 = Assert.IsType>(DotNetObjectReferenceManager.Current.FindDotNetObject(property.GetInt64())).Value; + var resultDto2 = Assert.IsType>(jsRuntime.GetObjectReference(property.GetInt64())).Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(1299, resultDto2.IntVal); - }); + } [Fact] - public Task InvokingWithIncorrectUseOfDotNetObjectRefThrows() => WithJSRuntime(jsRuntime => + public void InvokingWithIncorrectUseOfDotNetObjectRefThrows() { // Arrange + var jsRuntime = new TestJSRuntime(); var method = nameof(SomePublicType.IncorrectDotNetObjectRefUsage); var arg3 = new TestDTO { IntVal = 999, StringVal = "My string" }; var objectRef = DotNetObjectReference.Create(arg3); @@ -162,67 +167,72 @@ namespace Microsoft.JSInterop.Infrastructure new TestDTO { StringVal = "Another string", IntVal = 456 }, new[] { 100, 200 }, objectRef - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act & Assert var ex = Assert.Throws(() => - DotNetDispatcher.Invoke(thisAssemblyName, method, default, argsJson)); + DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, method, default, argsJson)); Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 3 must be declared as type 'DotNetObjectRef' to receive the incoming value.", ex.Message); - }); + } [Fact] - public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeInstanceVoidMethod() { // Arrange: Track some instance + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null); // Assert Assert.Null(resultJson); Assert.True(targetInstance.DidInvokeMyInvocableInstanceVoid); - }); + } [Fact] - public Task CanInvokeBaseInstanceVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeBaseInstanceVoidMethod() { // Arrange: Track some instance + var jsRuntime = new TestJSRuntime(); var targetInstance = new DerivedClass(); var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "BaseClassInvokableInstanceVoid", 1, null); // Assert Assert.Null(resultJson); Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid); - }); + } [Fact] - public Task DotNetObjectReferencesCanBeDisposed() => WithJSRuntime(jsRuntime => + public void DotNetObjectReferencesCanBeDisposed() { // Arrange + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var objectRef = DotNetObjectReference.Create(targetInstance); + jsRuntime.Invoke("unimportant", objectRef); // Act - DotNetDispatcher.BeginInvokeDotNet(null, null, "__Dispose", objectRef.ObjectId, null); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, null, null, "__Dispose", objectRef.ObjectId, null); // Assert Assert.True(objectRef.Disposed); - }); + } [Fact] - public Task CannotUseDotNetObjectRefAfterDisposal() => WithJSRuntime(jsRuntime => + public void CannotUseDotNetObjectRefAfterDisposal() { // This test addresses the case where the developer calls objectRef.Dispose() // from .NET code, as opposed to .dispose() from JS code // Arrange: Track some instance, then dispose it + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); @@ -230,17 +240,18 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); + () => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); - }); + } [Fact] - public Task CannotUseDotNetObjectRefAfterReleaseDotNetObject() => WithJSRuntime(jsRuntime => + public void CannotUseDotNetObjectRefAfterReleaseDotNetObject() { // This test addresses the case where the developer calls .dispose() // from JS code, as opposed to objectRef.Dispose() from .NET code // Arrange: Track some instance, then dispose it + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); @@ -248,80 +259,85 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null)); + () => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); - }); + } [Fact] - public Task EndInvoke_WithSuccessValue() => WithJSRuntime(jsRuntime => + public void EndInvoke_WithSuccessValue() { // Arrange + var jsRuntime = new TestJSRuntime(); var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 }; var task = jsRuntime.InvokeAsync("unimportant"); - var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, JsonSerializerOptionsProvider.Options); + var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions); // Act - DotNetDispatcher.EndInvokeJS(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert Assert.True(task.IsCompletedSuccessfully); var result = task.Result; Assert.Equal(testDTO.StringVal, result.StringVal); Assert.Equal(testDTO.IntVal, result.IntVal); - }); + } [Fact] - public Task EndInvoke_WithErrorString() => WithJSRuntime(async jsRuntime => + public async Task EndInvoke_WithErrorString() { // Arrange + var jsRuntime = new TestJSRuntime(); var expected = "Some error"; var task = jsRuntime.InvokeAsync("unimportant"); - var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, JsonSerializerOptionsProvider.Options); + var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, expected }, jsRuntime.JsonSerializerOptions); // Act - DotNetDispatcher.EndInvokeJS(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert var ex = await Assert.ThrowsAsync(async () => await task); Assert.Equal(expected, ex.Message); - }); + } [Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12357")] - public Task EndInvoke_AfterCancel() => WithJSRuntime(jsRuntime => + public void EndInvoke_AfterCancel() { // Arrange + var jsRuntime = new TestJSRuntime(); var testDTO = new TestDTO { StringVal = "Hello", IntVal = 4 }; var cts = new CancellationTokenSource(); var task = jsRuntime.InvokeAsync("unimportant", cts.Token); - var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, JsonSerializerOptionsProvider.Options); + var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, true, testDTO }, jsRuntime.JsonSerializerOptions); // Act cts.Cancel(); - DotNetDispatcher.EndInvokeJS(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert Assert.True(task.IsCanceled); - }); + } [Fact] - public Task EndInvoke_WithNullError() => WithJSRuntime(async jsRuntime => + public async Task EndInvoke_WithNullError() { // Arrange + var jsRuntime = new TestJSRuntime(); var task = jsRuntime.InvokeAsync("unimportant"); - var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, JsonSerializerOptionsProvider.Options); + var argsJson = JsonSerializer.Serialize(new object[] { jsRuntime.LastInvocationAsyncHandle, false, null }, jsRuntime.JsonSerializerOptions); // Act - DotNetDispatcher.EndInvokeJS(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert var ex = await Assert.ThrowsAsync(async () => await task); Assert.Empty(ex.Message); - }); + } [Fact] - public Task CanInvokeInstanceMethodWithParams() => WithJSRuntime(jsRuntime => + public void CanInvokeInstanceMethodWithParams() { // Arrange: Track some instance plus another object we'll pass as a param + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; jsRuntime.Invoke("unimportant", @@ -330,38 +346,40 @@ namespace Microsoft.JSInterop.Infrastructure var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]"; // Act - var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceMethod", 1, argsJson); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceMethod", 1, argsJson); // Assert Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson); - var resultDto = ((DotNetObjectReference)jsRuntime.ObjectRefManager.FindDotNetObject(3)).Value; + var resultDto = ((DotNetObjectReference)jsRuntime.GetObjectReference(3)).Value; Assert.Equal(1235, resultDto.IntVal); Assert.Equal("MY STRING", resultDto.StringVal); - }); + } [Fact] - public Task CannotInvokeWithFewerNumberOfParameters() => WithJSRuntime(jsRuntime => + public void CannotInvokeWithFewerNumberOfParameters() { // Arrange + var jsRuntime = new TestJSRuntime(); var argsJson = JsonSerializer.Serialize(new object[] { new TestDTO { StringVal = "Another string", IntVal = 456 }, new[] { 100, 200 }, - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); }); Assert.Equal("The call to 'InvocableStaticWithParams' expects '3' parameters, but received '2'.", ex.Message); - }); + } [Fact] - public Task CannotInvokeWithMoreParameters() => WithJSRuntime(jsRuntime => + public void CannotInvokeWithMoreParameters() { // Arrange + var jsRuntime = new TestJSRuntime(); var objectRef = DotNetObjectReference.Create(new TestDTO { IntVal = 4 }); var argsJson = JsonSerializer.Serialize(new object[] { @@ -369,21 +387,22 @@ namespace Microsoft.JSInterop.Infrastructure new[] { 100, 200 }, objectRef, 7, - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); }); Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message); - }); + } [Fact] - public Task CanInvokeAsyncMethod() => WithJSRuntime(async jsRuntime => + public async Task CanInvokeAsyncMethod() { // Arrange: Track some instance plus another object we'll pass as a param + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); var arg2 = new TestDTO { IntVal = 1234, StringVal = "My string" }; var arg1Ref = DotNetObjectReference.Create(targetInstance); @@ -395,12 +414,12 @@ namespace Microsoft.JSInterop.Infrastructure { new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, arg2Ref, - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, null, "InvokableAsyncMethod", 1, argsJson); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableAsyncMethod", 1, argsJson); await resultTask; // Assert: Correct completion information @@ -417,17 +436,18 @@ namespace Microsoft.JSInterop.Infrastructure var resultDto2 = resultDto2Ref.Value; Assert.Equal("MY STRING", resultDto2.StringVal); Assert.Equal(2468, resultDto2.IntVal); - }); + } [Fact] - public Task CanInvokeSyncThrowingMethod() => WithJSRuntime(async jsRuntime => + public async Task CanInvokeSyncThrowingMethod() { // Arrange + var jsRuntime = new TestJSRuntime(); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -439,17 +459,18 @@ namespace Microsoft.JSInterop.Infrastructure // https://github.com/aspnet/AspNetCore/issues/8612 var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception); - }); + } [Fact] - public Task CanInvokeAsyncThrowingMethod() => WithJSRuntime(async jsRuntime => + public async Task CanInvokeAsyncThrowingMethod() { // Arrange + var jsRuntime = new TestJSRuntime(); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -461,15 +482,16 @@ namespace Microsoft.JSInterop.Infrastructure // https://github.com/aspnet/AspNetCore/issues/8612 var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); - }); + } [Fact] - public Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId() => WithJSRuntime(async jsRuntime => + public async Task BeginInvoke_ThrowsWithInvalidArgsJson_WithCallId() { // Arrange + var jsRuntime = new TestJSRuntime(); var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, thisAssemblyName, "InvocableStaticWithParams", default, "not json"); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, "InvocableStaticWithParams", default, "not json"); await resultTask; // This won't throw, it sets properties on the jsRuntime. @@ -478,29 +500,30 @@ namespace Microsoft.JSInterop.Infrastructure Assert.False(jsRuntime.LastCompletionStatus); // Fails var result = Assert.IsType(jsRuntime.LastCompletionResult); Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result.SourceException.ToString()); - }); + } [Fact] - public Task BeginInvoke_ThrowsWithInvalid_DotNetObjectRef() => WithJSRuntime(jsRuntime => + public void BeginInvoke_ThrowsWithInvalid_DotNetObjectRef() { // Arrange + var jsRuntime = new TestJSRuntime(); var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(callId, null, "InvokableInstanceVoid", 1, null); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableInstanceVoid", 1, null); // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); Assert.False(jsRuntime.LastCompletionStatus); // Fails var result = Assert.IsType(jsRuntime.LastCompletionResult); - Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectRef instance was already disposed.", result.SourceException.ToString()); - }); + Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectReference instance was already disposed.", result.SourceException.ToString()); + } [Theory] [InlineData("")] [InlineData("")] public void ParseArguments_ThrowsIfJsonIsInvalid(string arguments) { - Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) })); + Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) })); } [Theory] @@ -509,7 +532,7 @@ namespace Microsoft.JSInterop.Infrastructure public void ParseArguments_ThrowsIfTheArgsJsonIsNotArray(string arguments) { // Act & Assert - Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) })); + Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) })); } [Theory] @@ -518,7 +541,7 @@ namespace Microsoft.JSInterop.Infrastructure public void ParseArguments_ThrowsIfTheArgsJsonIsInvalidArray(string arguments) { // Act & Assert - Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string) })); + Assert.ThrowsAny(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string) })); } [Fact] @@ -528,7 +551,7 @@ namespace Microsoft.JSInterop.Infrastructure var arguments = "[\"Hello\", 2]"; // Act - var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(string), typeof(int), }); + var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(string), typeof(int), }); // Assert Assert.Equal(new object[] { "Hello", 2 }, result); @@ -541,7 +564,7 @@ namespace Microsoft.JSInterop.Infrastructure var arguments = "[{\"IntVal\": 7}]"; // Act - var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(TestDTO), }); + var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(TestDTO), }); // Assert var value = Assert.IsType(Assert.Single(result)); @@ -556,7 +579,7 @@ namespace Microsoft.JSInterop.Infrastructure var arguments = "[4, null]"; // Act - var result = DotNetDispatcher.ParseArguments("SomeMethod", arguments, new[] { typeof(int), typeof(TestDTO), }); + var result = DotNetDispatcher.ParseArguments(new TestJSRuntime(), "SomeMethod", arguments, new[] { typeof(int), typeof(TestDTO), }); // Assert Assert.Collection( @@ -573,92 +596,72 @@ namespace Microsoft.JSInterop.Infrastructure var arguments = "[4, {\"__dotNetObject\": 7}]"; // Act - var ex = Assert.Throws(() => DotNetDispatcher.ParseArguments(method, arguments, new[] { typeof(int), typeof(TestDTO), })); + var ex = Assert.Throws(() => DotNetDispatcher.ParseArguments(new TestJSRuntime(), method, arguments, new[] { typeof(int), typeof(TestDTO), })); // Assert Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 2 must be declared as type 'DotNetObjectRef' to receive the incoming value.", ex.Message); } [Fact] - public void ParseEndInvokeArguments_ThrowsIfJsonIsEmptyString() + public void EndInvokeJS_ThrowsIfJsonIsEmptyString() { - Assert.ThrowsAny(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "")); + Assert.ThrowsAny(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "")); } [Fact] - public void ParseEndInvokeArguments_ThrowsIfJsonIsNotArray() + public void EndInvokeJS_ThrowsIfJsonIsNotArray() { - Assert.ThrowsAny(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "{\"key\": \"value\"}")); + Assert.ThrowsAny(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "{\"key\": \"value\"}")); } [Fact] - public void ParseEndInvokeArguments_ThrowsIfJsonArrayIsInComplete() + public void EndInvokeJS_ThrowsIfJsonArrayIsInComplete() { - Assert.ThrowsAny(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "[7, false")); + Assert.ThrowsAny(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false")); } [Fact] - public void ParseEndInvokeArguments_ThrowsIfJsonArrayHasMoreThan3Arguments() + public void EndInvokeJS_ThrowsIfJsonArrayHasMoreThan3Arguments() { - Assert.ThrowsAny(() => DotNetDispatcher.ParseEndInvokeArguments(new TestJSRuntime(), "[7, false, \"Hello\", 5]")); + Assert.ThrowsAny(() => DotNetDispatcher.EndInvokeJS(new TestJSRuntime(), "[7, false, \"Hello\", 5]")); } [Fact] - public void ParseEndInvokeArguments_Works() + public void EndInvokeJS_Works() { var jsRuntime = new TestJSRuntime(); var task = jsRuntime.InvokeAsync("somemethod"); - DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]"); + DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, {{\"intVal\": 7}}]"); Assert.True(task.IsCompletedSuccessfully); Assert.Equal(7, task.Result.IntVal); } [Fact] - public void ParseEndInvokeArguments_WithArrayValue() + public void EndInvokeJS_WithArrayValue() { var jsRuntime = new TestJSRuntime(); var task = jsRuntime.InvokeAsync("somemethod"); - DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]"); + DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, [1, 2, 3]]"); Assert.True(task.IsCompletedSuccessfully); Assert.Equal(new[] { 1, 2, 3 }, task.Result); } [Fact] - public void ParseEndInvokeArguments_WithNullValue() + public void EndInvokeJS_WithNullValue() { var jsRuntime = new TestJSRuntime(); var task = jsRuntime.InvokeAsync("somemethod"); - DotNetDispatcher.ParseEndInvokeArguments(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]"); + DotNetDispatcher.EndInvokeJS(jsRuntime, $"[{jsRuntime.LastInvocationAsyncHandle}, true, null]"); Assert.True(task.IsCompletedSuccessfully); Assert.Null(task.Result); } - Task WithJSRuntime(Action testCode) - { - return WithJSRuntime(jsRuntime => - { - testCode(jsRuntime); - return Task.CompletedTask; - }); - } - - async Task WithJSRuntime(Func testCode) - { - // Since the tests rely on the asynclocal JSRuntime.Current, ensure we - // are on a distinct async context with a non-null JSRuntime.Current - await Task.Yield(); - - var runtime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(runtime); - await testCode(runtime); - } - internal class SomeInteralType { [JSInvokable("MethodOnInternalType")] public void MyMethod() { } diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs index 541ad2b025..8d055aea2c 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs @@ -2,91 +2,93 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Text.Json; -using System.Threading.Tasks; using Xunit; -using static Microsoft.JSInterop.TestJSRuntime; namespace Microsoft.JSInterop.Infrastructure { public class DotNetObjectReferenceJsonConverterTest { + private readonly JSRuntime JSRuntime = new TestJSRuntime(); + private JsonSerializerOptions JsonSerializerOptions => JSRuntime.JsonSerializerOptions; + [Fact] - public Task Read_Throws_IfJsonIsMissingDotNetObjectProperty() => WithJSRuntime(_ => + public void Read_Throws_IfJsonIsMissingDotNetObjectProperty() { // Arrange + var jsRuntime = new TestJSRuntime(); var dotNetObjectRef = DotNetObjectReference.Create(new TestModel()); var json = "{}"; // Act & Assert - var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json)); + var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, JsonSerializerOptions)); Assert.Equal("Required property __dotNetObject not found.", ex.Message); - }); + } [Fact] - public Task Read_Throws_IfJsonContainsUnknownContent() => WithJSRuntime(_ => + public void Read_Throws_IfJsonContainsUnknownContent() { // Arrange + var jsRuntime = new TestJSRuntime(); var dotNetObjectRef = DotNetObjectReference.Create(new TestModel()); var json = "{\"foo\":2}"; // Act & Assert - var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json)); + var ex = Assert.Throws(() => JsonSerializer.Deserialize>(json, JsonSerializerOptions)); Assert.Equal("Unexcepted JSON property foo.", ex.Message); - }); + } [Fact] - public Task Read_Throws_IfJsonIsIncomplete() => WithJSRuntime(_ => + public void Read_Throws_IfJsonIsIncomplete() { // Arrange var input = new TestModel(); var dotNetObjectRef = DotNetObjectReference.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = $"{{\"__dotNetObject\":{objectId}"; // Act & Assert - var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); + var ex = Record.Exception(() => JsonSerializer.Deserialize>(json, JsonSerializerOptions)); Assert.IsAssignableFrom(ex); - }); + } [Fact] - public Task Read_Throws_IfDotNetObjectIdAppearsMultipleTimes() => WithJSRuntime(_ => + public void Read_Throws_IfDotNetObjectIdAppearsMultipleTimes() { // Arrange var input = new TestModel(); var dotNetObjectRef = DotNetObjectReference.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = $"{{\"__dotNetObject\":{objectId},\"__dotNetObject\":{objectId}}}"; // Act & Assert - var ex = Record.Exception(() => JsonSerializer.Deserialize>(json)); + var ex = Record.Exception(() => JsonSerializer.Deserialize>(json, JsonSerializerOptions)); Assert.IsAssignableFrom(ex); - }); + } [Fact] - public Task Read_ReadsJson() => WithJSRuntime(_ => + public void Read_ReadsJson() { // Arrange var input = new TestModel(); var dotNetObjectRef = DotNetObjectReference.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = $"{{\"__dotNetObject\":{objectId}}}"; // Act - var deserialized = JsonSerializer.Deserialize>(json); + var deserialized = JsonSerializer.Deserialize>(json, JsonSerializerOptions); // Assert Assert.Same(input, deserialized.Value); Assert.Equal(objectId, deserialized.ObjectId); - }); - + } [Fact] - public Task Read_ReturnsTheCorrectInstance() => WithJSRuntime(_ => + public void Read_ReturnsTheCorrectInstance() { // Arrange // Track a few instances and verify that the deserialized value returns the correct value. @@ -95,23 +97,23 @@ namespace Microsoft.JSInterop.Infrastructure var ref1 = DotNetObjectReference.Create(instance1); var ref2 = DotNetObjectReference.Create(instance2); - var json = $"[{{\"__dotNetObject\":{ref2.ObjectId}}},{{\"__dotNetObject\":{ref1.ObjectId}}}]"; + var json = $"[{{\"__dotNetObject\":{JSRuntime.TrackObjectReference(ref1)}}},{{\"__dotNetObject\":{JSRuntime.TrackObjectReference(ref2)}}}]"; // Act - var deserialized = JsonSerializer.Deserialize[]>(json); + var deserialized = JsonSerializer.Deserialize[]>(json, JsonSerializerOptions); // Assert - Assert.Same(instance2, deserialized[0].Value); - Assert.Same(instance1, deserialized[1].Value); - }); + Assert.Same(instance1, deserialized[0].Value); + Assert.Same(instance2, deserialized[1].Value); + } [Fact] - public Task Read_ReadsJson_WithFormatting() => WithJSRuntime(_ => + public void Read_ReadsJson_WithFormatting() { // Arrange var input = new TestModel(); var dotNetObjectRef = DotNetObjectReference.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = @$"{{ @@ -119,27 +121,27 @@ namespace Microsoft.JSInterop.Infrastructure }}"; // Act - var deserialized = JsonSerializer.Deserialize>(json); + var deserialized = JsonSerializer.Deserialize>(json, JsonSerializerOptions); // Assert Assert.Same(input, deserialized.Value); Assert.Equal(objectId, deserialized.ObjectId); - }); + } [Fact] - public Task WriteJsonTwice_KeepsObjectId() => WithJSRuntime(_ => + public void WriteJsonTwice_KeepsObjectId() { // Arrange var dotNetObjectRef = DotNetObjectReference.Create(new TestModel()); // Act - var json1 = JsonSerializer.Serialize(dotNetObjectRef); - var json2 = JsonSerializer.Serialize(dotNetObjectRef); + var json1 = JsonSerializer.Serialize(dotNetObjectRef, JsonSerializerOptions); + var json2 = JsonSerializer.Serialize(dotNetObjectRef, JsonSerializerOptions); // Assert Assert.Equal($"{{\"__dotNetObject\":{dotNetObjectRef.ObjectId}}}", json1); Assert.Equal(json1, json2); - }); + } private class TestModel { diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs index 4054101258..a1caff595b 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs @@ -18,7 +18,6 @@ namespace Microsoft.JSInterop { NextResultJson = "{\"intValue\":123,\"stringValue\":\"Hello\"}" }; - JSRuntime.SetCurrentJSRuntime(runtime); // Act var syncResult = runtime.Invoke("test identifier 1", "arg1", 123, true); @@ -36,7 +35,6 @@ namespace Microsoft.JSInterop { // Arrange var runtime = new TestJSInProcessRuntime { NextResultJson = null }; - JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); var obj3 = new object(); @@ -60,9 +58,9 @@ namespace Microsoft.JSInterop Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3}}]", call.ArgsJson); // Assert: Objects were tracked - Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(1).Value); - Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(2).Value); - Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(3).Value); + Assert.Same(obj1, runtime.GetObjectReference(1).Value); + Assert.Same(obj2, runtime.GetObjectReference(2).Value); + Assert.Same(obj3, runtime.GetObjectReference(3).Value); } [Fact] @@ -73,7 +71,6 @@ namespace Microsoft.JSInterop { NextResultJson = "[{\"__dotNetObject\":2},{\"__dotNetObject\":1}]" }; - JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index 4e65ddeb0f..b102ecc0b5 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -14,23 +14,6 @@ namespace Microsoft.JSInterop { public class JSRuntimeTest { - #region this will be removed eventually - [Fact] - public async Task CanHaveDistinctJSRuntimeInstancesInEachAsyncContext() - { - var tasks = Enumerable.Range(0, 20).Select(async _ => - { - var jsRuntime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(jsRuntime); - await Task.Delay(50).ConfigureAwait(false); - Assert.Same(jsRuntime, JSRuntime.Current); - }); - - await Task.WhenAll(tasks); - Assert.Null(JSRuntime.Current); - } - #endregion - [Fact] public void DispatchesAsyncCallsWithDistinctAsyncHandles() { @@ -274,7 +257,6 @@ namespace Microsoft.JSInterop { // Arrange var runtime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); var obj2 = new object(); var obj3 = new object(); @@ -296,15 +278,15 @@ namespace Microsoft.JSInterop // Assert: Serialized as expected var call = runtime.BeginInvokeCalls.Single(); Assert.Equal("test identifier", call.Identifier); - Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":3},\"obj3\":{\"__dotNetObject\":4},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":2}}]", call.ArgsJson); + Assert.Equal("[{\"__dotNetObject\":1},{\"obj2\":{\"__dotNetObject\":2},\"obj3\":{\"__dotNetObject\":3},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":4}}]", call.ArgsJson); // Assert: Objects were tracked - Assert.Same(obj1Ref, runtime.ObjectRefManager.FindDotNetObject(1)); + Assert.Same(obj1Ref, runtime.GetObjectReference(1)); Assert.Same(obj1, obj1Ref.Value); - Assert.NotSame(obj1Ref, runtime.ObjectRefManager.FindDotNetObject(2)); - Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2).Value); - Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3).Value); - Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4).Value); + Assert.NotSame(obj1Ref, runtime.GetObjectReference(2)); + Assert.Same(obj2, runtime.GetObjectReference(2).Value); + Assert.Same(obj3, runtime.GetObjectReference(3).Value); + Assert.Same(obj1, runtime.GetObjectReference(4).Value); } [Fact] diff --git a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs index 48782fc4df..740f02b8da 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Threading.Tasks; namespace Microsoft.JSInterop { @@ -17,16 +16,5 @@ namespace Microsoft.JSInterop { throw new NotImplementedException(); } - - public static async Task WithJSRuntime(Action testCode) - { - // Since the tests rely on the asynclocal JSRuntime.Current, ensure we - // are on a distinct async context with a non-null JSRuntime.Current - await Task.Yield(); - - var runtime = new TestJSRuntime(); - JSRuntime.SetCurrentJSRuntime(runtime); - testCode(runtime); - } } } diff --git a/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs index 2e4defd1b7..0996795ca3 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/ref/Mono.WebAssembly.Interop.netstandard2.0.cs @@ -8,6 +8,7 @@ namespace Mono.WebAssembly.Interop public MonoWebAssemblyJSRuntime() { } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { } protected override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) { } + protected static void Initialize(Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime jsRuntime) { } protected override string InvokeJS(string identifier, string argsJson) { throw null; } public TRes InvokeUnmarshalled(string identifier) { throw null; } public TRes InvokeUnmarshalled(string identifier, T0 arg0) { throw null; } diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs index 0e292a3e3c..b6b01d754d 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs @@ -16,6 +16,25 @@ namespace Mono.WebAssembly.Interop /// public class MonoWebAssemblyJSRuntime : JSInProcessRuntime { + /// + /// Gets the used to perform operations using . + /// + private static MonoWebAssemblyJSRuntime Instance { get; set; } + + /// + /// Initializes the to be used to perform operations using . + /// + /// The instance. + protected static void Initialize(MonoWebAssemblyJSRuntime jsRuntime) + { + if (Instance != null) + { + throw new InvalidOperationException("MonoWebAssemblyJSRuntime has already been initialized."); + } + + Instance = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime)); + } + /// protected override string InvokeJS(string identifier, string argsJson) { @@ -34,11 +53,11 @@ namespace Mono.WebAssembly.Interop // Invoked via Mono's JS interop mechanism (invoke_method) private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson) - => DotNetDispatcher.Invoke(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); + => DotNetDispatcher.Invoke(Instance, assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); // Invoked via Mono's JS interop mechanism (invoke_method) private static void EndInvokeJS(string argsJson) - => DotNetDispatcher.EndInvokeJS(argsJson); + => DotNetDispatcher.EndInvokeJS(Instance, argsJson); // Invoked via Mono's JS interop mechanism (invoke_method) private static void BeginInvokeDotNet(string callId, string assemblyNameOrDotNetObjectId, string methodIdentifier, string argsJson) @@ -59,7 +78,7 @@ namespace Mono.WebAssembly.Interop assemblyName = assemblyNameOrDotNetObjectId; } - DotNetDispatcher.BeginInvokeDotNet(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); + DotNetDispatcher.BeginInvokeDotNet(Instance, callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); } protected override void EndInvokeDotNet( @@ -84,7 +103,7 @@ namespace Mono.WebAssembly.Interop // We pass 0 as the async handle because we don't want the JS-side code to // send back any notification (we're just providing a result for an existing async call) - var args = JsonSerializer.Serialize(new[] { callId, success, resultOrError }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var args = JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions); BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); } From d46d569b8122ec3337fcae6a6814a6fb316bc45c Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 16 Aug 2019 16:19:23 -0700 Subject: [PATCH 5/6] Simplify JSRuntime method signature (dotnet/extensions#2188) \n\nCommit migrated from https://github.com/dotnet/extensions/commit/9c392a92efa88707023d8f0a47681aee217a9c56 --- .../ref/Microsoft.JSInterop.netcoreapp3.0.cs | 29 ++++++- .../ref/Microsoft.JSInterop.netstandard2.0.cs | 29 ++++++- .../src/Infrastructure/DotNetDispatcher.cs | 46 +++++----- .../Infrastructure/DotNetInvocationInfo.cs | 48 +++++++++++ .../Infrastructure/DotNetInvocationResult.cs | 55 ++++++++++++ .../Microsoft.JSInterop/src/JSRuntime.cs | 16 +--- .../Infrastructure/DotNetDispatcherTest.cs | 86 ++++++++----------- .../test/JSInProcessRuntimeTest.cs | 5 +- .../Microsoft.JSInterop/test/JSRuntimeTest.cs | 24 +++--- .../Microsoft.JSInterop/test/TestJSRuntime.cs | 3 +- ...Mono.WebAssembly.Interop.netstandard2.0.cs | 2 +- .../src/MonoWebAssemblyJSRuntime.cs | 28 ++---- 12 files changed, 249 insertions(+), 122 deletions(-) create mode 100644 src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationInfo.cs create mode 100644 src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.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 953f8b0329..2d8c51caaf 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs @@ -50,7 +50,7 @@ namespace Microsoft.JSInterop protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); - protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId); + protected internal abstract void EndInvokeDotNet(Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, in Microsoft.JSInterop.Infrastructure.DotNetInvocationResult invocationResult); 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; } } @@ -72,8 +72,31 @@ namespace Microsoft.JSInterop.Infrastructure { public static partial class DotNetDispatcher { - public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { } public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } - public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, in Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationInfo + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationInfo(string assemblyName, string methodIdentifier, long dotNetObjectId, string callId) { throw null; } + public string AssemblyName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string CallId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public long DotNetObjectId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string MethodIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationResult + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationResult(System.Exception exception, string errorKind) { throw null; } + public DotNetInvocationResult(object result) { throw null; } + public string ErrorKind { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public object Result { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public bool Success { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { 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 953f8b0329..2d8c51caaf 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -50,7 +50,7 @@ namespace Microsoft.JSInterop protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); - protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId); + protected internal abstract void EndInvokeDotNet(Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, in Microsoft.JSInterop.Infrastructure.DotNetInvocationResult invocationResult); 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; } } @@ -72,8 +72,31 @@ namespace Microsoft.JSInterop.Infrastructure { public static partial class DotNetDispatcher { - public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { } + public static void BeginInvokeDotNet(Microsoft.JSInterop.JSRuntime jsRuntime, Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { } public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } - public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { throw null; } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, in Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationInfo + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationInfo(string assemblyName, string methodIdentifier, long dotNetObjectId, string callId) { throw null; } + public string AssemblyName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string CallId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public long DotNetObjectId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string MethodIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationResult + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationResult(System.Exception exception, string errorKind) { throw null; } + public DotNetInvocationResult(object result) { throw null; } + public string ErrorKind { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public object Result { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public bool Success { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index 92afc6278d..6a3a4f8d5f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -28,12 +28,10 @@ namespace Microsoft.JSInterop.Infrastructure /// Receives a call from JS to .NET, locating and invoking the specified method. /// /// The . - /// The assembly containing the method to be invoked. - /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. - /// For instance method calls, identifies the target object. + /// The . /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static string Invoke(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static string Invoke(JSRuntime jsRuntime, in DotNetInvocationInfo invocationInfo, 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 @@ -41,12 +39,12 @@ namespace Microsoft.JSInterop.Infrastructure // because there would be nobody to police that. This method *is* the police. IDotNetObjectReference targetInstance = default; - if (dotNetObjectId != default) + if (invocationInfo.DotNetObjectId != default) { - targetInstance = jsRuntime.GetObjectReference(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId); } - var syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson); + var syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson); if (syncResult == null) { return null; @@ -59,13 +57,10 @@ namespace Microsoft.JSInterop.Infrastructure /// Receives a call from JS to .NET, locating and invoking the specified method asynchronously. /// /// The . - /// A value identifying the asynchronous call that should be passed back with the result, or null if no result notification is required. - /// The assembly containing the method to be invoked. - /// The identifier of the method to be invoked. The method must be annotated with a matching this identifier string. - /// For instance method calls, identifies the target object. + /// The . /// A JSON representation of the parameters. /// A JSON representation of the return value, or null. - public static void BeginInvokeDotNet(JSRuntime jsRuntime, string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) + public static void BeginInvokeDotNet(JSRuntime jsRuntime, DotNetInvocationInfo invocationInfo, 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 @@ -75,18 +70,19 @@ namespace Microsoft.JSInterop.Infrastructure // Using ExceptionDispatchInfo here throughout because we want to always preserve // original stack traces. + var callId = invocationInfo.CallId; + object syncResult = null; ExceptionDispatchInfo syncException = null; IDotNetObjectReference targetInstance = null; - try { - if (dotNetObjectId != default) + if (invocationInfo.DotNetObjectId != default) { - targetInstance = jsRuntime.GetObjectReference(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId); } - syncResult = InvokeSynchronously(jsRuntime, assemblyName, methodIdentifier, targetInstance, argsJson); + syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson); } catch (Exception ex) { @@ -101,7 +97,7 @@ namespace Microsoft.JSInterop.Infrastructure else if (syncException != null) { // Threw synchronously, let's respond. - jsRuntime.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(syncException.SourceException, "InvocationFailure")); } else if (syncResult is Task task) { @@ -111,23 +107,27 @@ namespace Microsoft.JSInterop.Infrastructure { if (t.Exception != null) { - var exception = t.Exception.GetBaseException(); - - jsRuntime.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier, dotNetObjectId); + var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(t.Exception.GetBaseException()); + var dispatchResult = new DotNetInvocationResult(exceptionDispatchInfo.SourceException, "InvocationFailure"); + jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult); } var result = TaskGenericsUtil.GetTaskResult(task); - jsRuntime.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(result)); }, TaskScheduler.Current); } else { - jsRuntime.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId); + var dispatchResult = new DotNetInvocationResult(syncResult); + jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult); } } - private static object InvokeSynchronously(JSRuntime jsRuntime, string assemblyName, string methodIdentifier, IDotNetObjectReference objectReference, string argsJson) + private static object InvokeSynchronously(JSRuntime jsRuntime, in DotNetInvocationInfo callInfo, IDotNetObjectReference objectReference, string argsJson) { + var assemblyName = callInfo.AssemblyName; + var methodIdentifier = callInfo.MethodIdentifier; + AssemblyKey assemblyKey; if (objectReference is null) { diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationInfo.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationInfo.cs new file mode 100644 index 0000000000..942fc34da0 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationInfo.cs @@ -0,0 +1,48 @@ +// 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. + +namespace Microsoft.JSInterop.Infrastructure +{ + /// + /// Information about a JSInterop call from JavaScript to .NET. + /// + public readonly struct DotNetInvocationInfo + { + /// + /// Initializes a new instance of . + /// + /// The name of the assembly containing the method. + /// The identifier of the method to be invoked. + /// The object identifier for instance method calls. + /// The call identifier. + public DotNetInvocationInfo(string assemblyName, string methodIdentifier, long dotNetObjectId, string callId) + { + CallId = callId; + AssemblyName = assemblyName; + MethodIdentifier = methodIdentifier; + DotNetObjectId = dotNetObjectId; + } + + /// + /// Gets the name of the assembly containing the method. + /// Only one of or may be specified. + /// + public string AssemblyName { get; } + + /// + /// Gets the identifier of the method to be invoked. This is the value specified in the . + /// + public string MethodIdentifier { get; } + + /// + /// Gets the object identifier for instance method calls. + /// Only one of or may be specified. + /// + public long DotNetObjectId { get; } + + /// + /// Gets the call identifier. This value is when the client does not expect a value to be returned. + /// + public string CallId { get; } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.cs new file mode 100644 index 0000000000..d62dd532ee --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetInvocationResult.cs @@ -0,0 +1,55 @@ +using System; + +namespace Microsoft.JSInterop.Infrastructure +{ + /// + /// Result of a .NET invocation that is returned to JavaScript. + /// + public readonly struct DotNetInvocationResult + { + /// + /// Constructor for a failed invocation. + /// + /// The that caused the failure. + /// The error kind. + public DotNetInvocationResult(Exception exception, string errorKind) + { + Result = default; + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + ErrorKind = errorKind; + Success = false; + } + + /// + /// Constructor for a successful invocation. + /// + /// The result. + public DotNetInvocationResult(object result) + { + Result = result; + Exception = default; + ErrorKind = default; + Success = true; + } + + /// + /// Gets the that caused the failure. + /// + public Exception Exception { get; } + + /// + /// Gets the error kind. + /// + public string ErrorKind { get; } + + /// + /// Gets the result of a successful invocation. + /// + public object Result { get; } + + /// + /// if the invocation succeeded, otherwise . + /// + public bool Success { get; } + } +} diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index ba411b72db..4dca7a5db3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -150,19 +150,11 @@ namespace Microsoft.JSInterop /// /// 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. + /// The . + /// The . protected internal abstract void EndInvokeDotNet( - string callId, - bool success, - object resultOrError, - string assemblyName, - string methodIdentifier, - long dotNetObjectId); + DotNetInvocationInfo invocationInfo, + in DotNetInvocationResult invocationResult); internal void EndInvokeJS(long taskId, bool succeeded, ref Utf8JsonReader jsonReader) { diff --git a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index 2d8208b7a6..7e82a47a89 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs @@ -20,7 +20,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(new TestJSRuntime(), " ", "SomeMethod", default, "[]"); + DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(" ", "SomeMethod", default, default), "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -32,7 +32,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(new TestJSRuntime(), "SomeAssembly", " ", default, "[]"); + DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo("SomeAssembly", " ", default, default), "[]"); }); Assert.StartsWith("Cannot be null, empty, or whitespace.", ex.Message); @@ -45,7 +45,7 @@ namespace Microsoft.JSInterop.Infrastructure var assemblyName = "Some.Fake.Assembly"; var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(new TestJSRuntime(), assemblyName, "SomeMethod", default, null); + DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(assemblyName, "SomeMethod", default, default), null); }); Assert.Equal($"There is no loaded assembly with the name '{assemblyName}'.", ex.Message); @@ -67,7 +67,7 @@ namespace Microsoft.JSInterop.Infrastructure { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(new TestJSRuntime(), thisAssemblyName, methodIdentifier, default, null); + DotNetDispatcher.Invoke(new TestJSRuntime(), new DotNetInvocationInfo(thisAssemblyName, methodIdentifier, default, default), null); }); Assert.Equal($"The assembly '{thisAssemblyName}' does not contain a public method with [JSInvokableAttribute(\"{methodIdentifier}\")].", ex.Message); @@ -79,7 +79,7 @@ namespace Microsoft.JSInterop.Infrastructure // Arrange/Act var jsRuntime = new TestJSRuntime(); SomePublicType.DidInvokeMyInvocableStaticVoid = false; - var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticVoid", default, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticVoid", default, default), null); // Assert Assert.Null(resultJson); @@ -91,7 +91,7 @@ namespace Microsoft.JSInterop.Infrastructure { // Arrange/Act var jsRuntime = new TestJSRuntime(); - var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticNonVoid", default, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticNonVoid", default, default), null); var result = JsonSerializer.Deserialize(resultJson, jsRuntime.JsonSerializerOptions); // Assert @@ -104,7 +104,7 @@ namespace Microsoft.JSInterop.Infrastructure { // Arrange/Act var jsRuntime = new TestJSRuntime(); - var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, default), null); var result = JsonSerializer.Deserialize(resultJson, jsRuntime.JsonSerializerOptions); // Assert @@ -130,7 +130,7 @@ namespace Microsoft.JSInterop.Infrastructure }, jsRuntime.JsonSerializerOptions); // Act - var resultJson = DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson); var result = JsonDocument.Parse(resultJson); var root = result.RootElement; @@ -171,7 +171,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act & Assert var ex = Assert.Throws(() => - DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, method, default, argsJson)); + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, method, default, default), argsJson)); Assert.Equal($"In call to '{method}', parameter of type '{nameof(TestDTO)}' at index 3 must be declared as type 'DotNetObjectRef' to receive the incoming value.", ex.Message); } @@ -185,7 +185,7 @@ namespace Microsoft.JSInterop.Infrastructure jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null); // Assert Assert.Null(resultJson); @@ -202,7 +202,7 @@ namespace Microsoft.JSInterop.Infrastructure jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "BaseClassInvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "BaseClassInvokableInstanceVoid", 1, default), null); // Assert Assert.Null(resultJson); @@ -219,7 +219,7 @@ namespace Microsoft.JSInterop.Infrastructure jsRuntime.Invoke("unimportant", objectRef); // Act - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, null, null, "__Dispose", objectRef.ObjectId, null); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "__Dispose", objectRef.ObjectId, default), null); // Assert Assert.True(objectRef.Disposed); @@ -240,7 +240,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null)); + () => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); } @@ -259,7 +259,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceVoid", 1, null)); + () => DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), null)); Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); } @@ -346,7 +346,7 @@ namespace Microsoft.JSInterop.Infrastructure var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]"; // Act - var resultJson = DotNetDispatcher.Invoke(jsRuntime, null, "InvokableInstanceMethod", 1, argsJson); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceMethod", 1, default), argsJson); // Assert Assert.Equal("[\"You passed myvalue\",{\"__dotNetObject\":3}]", resultJson); @@ -369,7 +369,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson); }); Assert.Equal("The call to 'InvocableStaticWithParams' expects '3' parameters, but received '2'.", ex.Message); @@ -392,7 +392,7 @@ namespace Microsoft.JSInterop.Infrastructure // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(jsRuntime, thisAssemblyName, "InvocableStaticWithParams", default, argsJson); + DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, default), argsJson); }); Assert.Equal("Unexpected JSON token Number. Ensure that the call to `InvocableStaticWithParams' is supplied with exactly '3' parameters.", ex.Message); @@ -419,13 +419,13 @@ namespace Microsoft.JSInterop.Infrastructure // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableAsyncMethod", 1, argsJson); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "InvokableAsyncMethod", 1, callId), argsJson); await resultTask; // Assert: Correct completion information Assert.Equal(callId, jsRuntime.LastCompletionCallId); - Assert.True(jsRuntime.LastCompletionStatus); - var result = Assert.IsType(jsRuntime.LastCompletionResult); + Assert.True(jsRuntime.LastCompletionResult.Success); + var result = Assert.IsType(jsRuntime.LastCompletionResult.Result); var resultDto1 = Assert.IsType(result[0]); Assert.Equal("STRING VIA JSON", resultDto1.StringVal); @@ -447,18 +447,17 @@ namespace Microsoft.JSInterop.Infrastructure // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(ThrowingClass.ThrowingMethod), default, callId), default); await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); - Assert.False(jsRuntime.LastCompletionStatus); // Fails + Assert.False(jsRuntime.LastCompletionResult.Success); // Fails // Make sure the method that threw the exception shows up in the call stack // https://github.com/aspnet/AspNetCore/issues/8612 - var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; - Assert.Contains(nameof(ThrowingClass.ThrowingMethod), exception); + Assert.Contains(nameof(ThrowingClass.ThrowingMethod), jsRuntime.LastCompletionResult.Exception.ToString()); } [Fact] @@ -470,18 +469,17 @@ namespace Microsoft.JSInterop.Infrastructure // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, default); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, nameof(ThrowingClass.AsyncThrowingMethod), default, callId), default); await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); - Assert.False(jsRuntime.LastCompletionStatus); // Fails + Assert.False(jsRuntime.LastCompletionResult.Success); // Fails // Make sure the method that threw the exception shows up in the call stack // https://github.com/aspnet/AspNetCore/issues/8612 - var exception = jsRuntime.LastCompletionResult is ExceptionDispatchInfo edi ? edi.SourceException.ToString() : null; - Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), exception); + Assert.Contains(nameof(ThrowingClass.AsyncThrowingMethod), jsRuntime.LastCompletionResult.Exception.ToString()); } [Fact] @@ -491,15 +489,15 @@ namespace Microsoft.JSInterop.Infrastructure var jsRuntime = new TestJSRuntime(); var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, thisAssemblyName, "InvocableStaticWithParams", default, "not json"); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, callId), "not json"); await resultTask; // This won't throw, it sets properties on the jsRuntime. // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); - Assert.False(jsRuntime.LastCompletionStatus); // Fails - var result = Assert.IsType(jsRuntime.LastCompletionResult); - Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result.SourceException.ToString()); + Assert.False(jsRuntime.LastCompletionResult.Success); // Fails + var exception = jsRuntime.LastCompletionResult.Exception; + Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", exception.ToString()); } [Fact] @@ -509,13 +507,13 @@ namespace Microsoft.JSInterop.Infrastructure var jsRuntime = new TestJSRuntime(); var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvokeDotNet(jsRuntime, callId, null, "InvokableInstanceVoid", 1, null); + DotNetDispatcher.BeginInvokeDotNet(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, callId), null); // Assert Assert.Equal(callId, jsRuntime.LastCompletionCallId); - Assert.False(jsRuntime.LastCompletionStatus); // Fails - var result = Assert.IsType(jsRuntime.LastCompletionResult); - Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectReference instance was already disposed.", result.SourceException.ToString()); + Assert.False(jsRuntime.LastCompletionResult.Success); // Fails + var exception = jsRuntime.LastCompletionResult.Exception; + Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectReference instance was already disposed.", exception.ToString()); } [Theory] @@ -801,8 +799,7 @@ namespace Microsoft.JSInterop.Infrastructure public string LastInvocationArgsJson { get; private set; } public string LastCompletionCallId { get; private set; } - public bool LastCompletionStatus { get; private set; } - public object LastCompletionResult { get; private set; } + public DotNetInvocationResult LastCompletionResult { get; private set; } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { @@ -823,17 +820,10 @@ namespace Microsoft.JSInterop.Infrastructure return null; } - protected internal override void EndInvokeDotNet( - string callId, - bool success, - object resultOrError, - string assemblyName, - string methodIdentifier, - long dotNetObjectId) + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { - LastCompletionCallId = callId; - LastCompletionStatus = success; - LastCompletionResult = resultOrError; + LastCompletionCallId = invocationInfo.CallId; + LastCompletionResult = invocationResult; _nextInvocationTcs.SetResult(null); _nextInvocationTcs = new TaskCompletionSource(); } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs index a1caff595b..f42e0801a0 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.JSInterop.Infrastructure; using Xunit; namespace Microsoft.JSInterop @@ -113,8 +114,8 @@ namespace Microsoft.JSInterop protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) => throw new NotImplementedException("This test only covers sync calls"); - protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) => - throw new NotImplementedException("This test only covers sync calls"); + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + => throw new NotImplementedException("This test only covers sync calls"); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index b102ecc0b5..66e0033d2a 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.JSInterop.Infrastructure; using Xunit; namespace Microsoft.JSInterop @@ -296,18 +297,20 @@ namespace Microsoft.JSInterop 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 " + + string GetMessage(DotNetInvocationInfo info) => $"An error ocurred while invoking '[{info.AssemblyName}]::{info.MethodIdentifier}'. 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) } + OnDotNetException = (invocationInfo) => new JSError { Message = GetMessage(invocationInfo) } }; var exception = new Exception("Some really sensitive data in here"); + var invocation = new DotNetInvocationInfo("Assembly", "Method", 0, "0"); + var result = new DotNetInvocationResult(exception, default); // Act - runtime.EndInvokeDotNet("0", false, exception, "Assembly", "Method", 0); + runtime.EndInvokeDotNet(invocation, result); // Assert var call = runtime.EndInvokeDotNetCalls.Single(); @@ -356,20 +359,21 @@ namespace Microsoft.JSInterop public object ResultOrError { get; set; } } - public Func OnDotNetException { get; set; } + public Func OnDotNetException { get; set; } - protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { - if (OnDotNetException != null && !success) + var resultOrError = invocationResult.Success ? invocationResult.Result : invocationResult.Exception; + if (OnDotNetException != null && !invocationResult.Success) { - resultOrError = OnDotNetException(resultOrError as Exception, assemblyName, methodIdentifier); + resultOrError = OnDotNetException(invocationInfo); } EndInvokeDotNetCalls.Add(new EndInvokeDotNetArgs { - CallId = callId, - Success = success, - ResultOrError = resultOrError + CallId = invocationInfo.CallId, + Success = invocationResult.Success, + ResultOrError = resultOrError, }); } diff --git a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs index 740f02b8da..db9c5ddd36 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop { @@ -12,7 +13,7 @@ namespace Microsoft.JSInterop throw new NotImplementedException(); } - protected internal override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) + protected internal override void EndInvokeDotNet(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) { throw new NotImplementedException(); } 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 0996795ca3..8dd70b946a 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 @@ -7,7 +7,7 @@ namespace Mono.WebAssembly.Interop { public MonoWebAssemblyJSRuntime() { } protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { } - protected override void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId) { } + protected override void EndInvokeDotNet(Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo callInfo, in Microsoft.JSInterop.Infrastructure.DotNetInvocationResult dispatchResult) { } protected static void Initialize(Mono.WebAssembly.Interop.MonoWebAssemblyJSRuntime jsRuntime) { } protected override string InvokeJS(string identifier, string argsJson) { throw null; } public TRes InvokeUnmarshalled(string identifier) { throw null; } diff --git a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs index b6b01d754d..654263a123 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Runtime.ExceptionServices; using System.Text.Json; using Microsoft.JSInterop; using Microsoft.JSInterop.Infrastructure; @@ -53,7 +52,10 @@ namespace Mono.WebAssembly.Interop // Invoked via Mono's JS interop mechanism (invoke_method) private static string InvokeDotNet(string assemblyName, string methodIdentifier, string dotNetObjectId, string argsJson) - => DotNetDispatcher.Invoke(Instance, assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), argsJson); + { + var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId == null ? default : long.Parse(dotNetObjectId), callId: null); + return DotNetDispatcher.Invoke(Instance, callInfo, argsJson); + } // Invoked via Mono's JS interop mechanism (invoke_method) private static void EndInvokeJS(string argsJson) @@ -78,32 +80,20 @@ namespace Mono.WebAssembly.Interop assemblyName = assemblyNameOrDotNetObjectId; } - DotNetDispatcher.BeginInvokeDotNet(Instance, callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); + var callInfo = new DotNetInvocationInfo(assemblyName, methodIdentifier, dotNetObjectId, callId); + DotNetDispatcher.BeginInvokeDotNet(Instance, callInfo, argsJson); } - protected override void EndInvokeDotNet( - string callId, - bool success, - object resultOrError, - string assemblyName, - string methodIdentifier, - long dotNetObjectId) + protected override void EndInvokeDotNet(DotNetInvocationInfo callInfo, in DotNetInvocationResult dispatchResult) { // For failures, the common case is to call EndInvokeDotNet with the Exception object. // For these we'll serialize as something that's useful to receive on the JS side. // If the value is not an Exception, we'll just rely on it being directly JSON-serializable. - if (!success && resultOrError is Exception ex) - { - resultOrError = ex.ToString(); - } - else if (!success && resultOrError is ExceptionDispatchInfo edi) - { - resultOrError = edi.SourceException.ToString(); - } + var resultOrError = dispatchResult.Success ? dispatchResult.Result : dispatchResult.Exception.ToString(); // We pass 0 as the async handle because we don't want the JS-side code to // send back any notification (we're just providing a result for an existing async call) - var args = JsonSerializer.Serialize(new[] { callId, success, resultOrError }, JsonSerializerOptions); + var args = JsonSerializer.Serialize(new[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions); BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); } From bd300b52204aa73283e9cdd081ee3c0a8c18a7bf Mon Sep 17 00:00:00 2001 From: Doug Bunting <6431421+dougbu@users.noreply.github.com> Date: Fri, 16 Aug 2019 18:28:51 -0700 Subject: [PATCH 6/6] Rebuilt ref/ code for Microsoft.JSInterop \n\nCommit migrated from https://github.com/dotnet/extensions/commit/b791bce755e554c0092fe0c7b39dc06289ad229a --- .../ref/Microsoft.JSInterop.netcoreapp5.0.cs | 92 +++++++++++++------ 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp5.0.cs b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp5.0.cs index 654ae9d617..2d8c51caaf 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp5.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp5.0.cs @@ -3,22 +3,14 @@ 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; } - [Microsoft.JSInterop.JSInvokableAttribute("DotNetDispatcher.ReleaseDotNetObject")] - public static void ReleaseDotNetObject(long dotNetObjectId) { } + 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() { } - public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + internal DotNetObjectReference() { } + public TValue Value { get { throw null; } } public void Dispose() { } } public partial interface IJSInProcessRuntime : Microsoft.JSInterop.IJSRuntime @@ -27,38 +19,84 @@ 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 internal System.Text.Json.JsonSerializerOptions JsonSerializerOptions { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); - protected internal abstract void EndInvokeDotNet(string callId, bool success, object resultOrError, string assemblyName, string methodIdentifier, long dotNetObjectId); - public System.Threading.Tasks.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; } + protected internal abstract void EndInvokeDotNet(Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, in Microsoft.JSInterop.Infrastructure.DotNetInvocationResult invocationResult); + 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 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(Microsoft.JSInterop.JSRuntime jsRuntime, Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { } + public static void EndInvokeJS(Microsoft.JSInterop.JSRuntime jsRuntime, string arguments) { } + public static string Invoke(Microsoft.JSInterop.JSRuntime jsRuntime, in Microsoft.JSInterop.Infrastructure.DotNetInvocationInfo invocationInfo, string argsJson) { throw null; } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationInfo + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationInfo(string assemblyName, string methodIdentifier, long dotNetObjectId, string callId) { throw null; } + public string AssemblyName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string CallId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public long DotNetObjectId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string MethodIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + } + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct DotNetInvocationResult + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public DotNetInvocationResult(System.Exception exception, string errorKind) { throw null; } + public DotNetInvocationResult(object result) { throw null; } + public string ErrorKind { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public object Result { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public bool Success { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } }