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..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,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[] | null): 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, 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 654ae9d617..2d8c51caaf 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netcoreapp3.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; } } } } 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; } } } } 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..2d8c51caaf 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.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; } } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.cs deleted file mode 100644 index ad1469e38f..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefManager.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 -{ - 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(); - - public static DotNetObjectRefManager Current - { - get - { - if (!(JSRuntime.Current is JSRuntimeBase jsRuntimeBase)) - { - throw new InvalidOperationException("JSRuntime must be set up correctly and must be an instance of JSRuntimeBase to use DotNetObjectRef."); - } - - return jsRuntimeBase.ObjectRefManager; - } - } - - public long TrackObject(object dotNetObjectRef) - { - var dotNetObjectId = Interlocked.Increment(ref _nextId); - _trackedRefsById[dotNetObjectId] = dotNetObjectRef; - - return dotNetObjectId; - } - - public object 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/DotNetObjectRefOfT.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs deleted file mode 100644 index be6bf91663..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefOfT.cs +++ /dev/null @@ -1,47 +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.Text.Json.Serialization; - -namespace Microsoft.JSInterop -{ - /// - /// Wraps a JS interop argument, indicating that the value should not be serialized as JSON - /// but instead should be passed as a reference. - /// - /// 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 DotNetObjectRef : IDotNetObjectRef, IDisposable where TValue : class - { - /// - /// Initializes a new instance of . - /// - /// The object Id. - /// The value to pass by reference. - internal DotNetObjectRef(long objectId, TValue value) - { - ObjectId = objectId; - Value = value; - } - - /// - /// Gets the object instance represented by this wrapper. - /// - public TValue Value { get; } - - internal long ObjectId { get; } - - /// - /// Stops tracking this object reference, allowing it to be garbage collected - /// (if there are no other references to it). Once the instance is disposed, it - /// can no longer be used in interop calls from JavaScript code. - /// - public void Dispose() - { - DotNetObjectRefManager.Current.ReleaseDotNetObject(ObjectId); - } - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs similarity index 55% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs rename to src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs index af790281e9..989d8062bb 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReference.cs @@ -4,19 +4,18 @@ 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 { - var objectId = DotNetObjectRefManager.Current.TrackObject(value); - return new DotNetObjectRef(objectId, value); + return new DotNetObjectReference(value); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs new file mode 100644 index 0000000000..773c2ed9a3 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceOfT.cs @@ -0,0 +1,105 @@ +// 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.Diagnostics; +using Microsoft.JSInterop.Infrastructure; + +namespace Microsoft.JSInterop +{ + /// + /// Wraps a JS interop argument, indicating that the value should not be serialized as JSON + /// but instead should be passed as a reference. + /// + /// To avoid leaking memory, the reference must later be disposed by JS code or by .NET code. + /// + /// The type of the value to wrap. + public sealed class DotNetObjectReference : IDotNetObjectReference, IDisposable where TValue : class + { + private readonly TValue _value; + private long _objectId; + private JSRuntime _jsRuntime; + + /// + /// Initializes a new instance of . + /// + /// The value to pass by reference. + internal DotNetObjectReference(TValue value) + { + _value = value; + } + + /// + /// Gets the object instance represented by this wrapper. + /// + public TValue Value + { + get + { + ThrowIfDisposed(); + return _value; + } + } + + internal long ObjectId + { + 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; + + internal bool Disposed { get; private set; } + + /// + /// Stops tracking this object reference, allowing it to be garbage collected + /// (if there are no other references to it). Once the instance is disposed, it + /// can no longer be used in interop calls from JavaScript code. + /// + public void Dispose() + { + if (!Disposed) + { + Disposed = true; + + if (_jsRuntime != null) + { + _jsRuntime.ReleaseObjectReference(_objectId); + } + } + } + + internal void ThrowIfDisposed() + { + if (Disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + } +} 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 74% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs index 0dfac228a6..6a3a4f8d5f 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetDispatcher.cs @@ -11,13 +11,14 @@ 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. /// public static class DotNetDispatcher { + private const string DisposeDotNetObjectReferenceMethodName = "__Dispose"; internal static readonly JsonEncodedText DotNetObjectRefKey = JsonEncodedText.Encode("__dotNetObject"); private static readonly ConcurrentDictionary> _cachedMethodsByAssembly @@ -26,68 +27,62 @@ namespace Microsoft.JSInterop /// /// Receives a call from JS to .NET, locating and invoking the specified method. /// - /// 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 . + /// The . /// 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, 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 // 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; - if (dotNetObjectId != default) + IDotNetObjectReference targetInstance = default; + if (invocationInfo.DotNetObjectId != default) { - targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId); } - var syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + var syncResult = InvokeSynchronously(jsRuntime, invocationInfo, 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. /// - /// 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 . + /// The . /// 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(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 // 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 = (JSRuntimeBase)JSRuntime.Current; - // Using ExceptionDispatchInfo here throughout because we want to always preserve // original stack traces. + + var callId = invocationInfo.CallId; + object syncResult = null; ExceptionDispatchInfo syncException = null; - object targetInstance = null; - + IDotNetObjectReference targetInstance = null; try { - if (dotNetObjectId != default) + if (invocationInfo.DotNetObjectId != default) { - targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); + targetInstance = jsRuntime.GetObjectReference(invocationInfo.DotNetObjectId); } - syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson); + syncResult = InvokeSynchronously(jsRuntime, invocationInfo, targetInstance, argsJson); } catch (Exception ex) { @@ -102,7 +97,7 @@ namespace Microsoft.JSInterop else if (syncException != null) { // Threw synchronously, let's respond. - jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(syncException.SourceException, "InvocationFailure")); } else if (syncResult is Task task) { @@ -112,45 +107,57 @@ namespace Microsoft.JSInterop { if (t.Exception != null) { - var exception = t.Exception.GetBaseException(); - - jsRuntimeBaseInstance.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); - jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier, dotNetObjectId); + jsRuntime.EndInvokeDotNet(invocationInfo, new DotNetInvocationResult(result)); }, TaskScheduler.Current); } else { - jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier, dotNetObjectId); + var dispatchResult = new DotNetInvocationResult(syncResult); + jsRuntime.EndInvokeDotNet(invocationInfo, dispatchResult); } } - private static object InvokeSynchronously(string assemblyName, string methodIdentifier, object targetInstance, 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 (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); - var suppliedArgs = ParseArguments(methodIdentifier, argsJson, parameterTypes); + var suppliedArgs = ParseArguments(jsRuntime, methodIdentifier, argsJson, parameterTypes); 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 { @@ -164,7 +171,7 @@ namespace Microsoft.JSInterop } } - 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) { @@ -189,7 +196,7 @@ namespace Microsoft.JSInterop 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++; } @@ -218,7 +225,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; @@ -230,26 +237,21 @@ 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 /// 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 EndInvoke(string arguments) - { - var jsRuntimeBase = (JSRuntimeBase)JSRuntime.Current; - ParseEndInvokeArguments(jsRuntimeBase, arguments); - } - - internal static void ParseEndInvokeArguments(JSRuntimeBase jsRuntimeBase, string arguments) + public static void EndInvokeJS(JSRuntime jsRuntime, string arguments) { var utf8JsonBytes = Encoding.UTF8.GetBytes(arguments); @@ -272,7 +274,7 @@ namespace Microsoft.JSInterop 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) { @@ -280,22 +282,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/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/DotNetObjectReferenceJsonConverter.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs similarity index 67% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs index eaabdbf9e6..7658bbc2c3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectReferenceJsonConverter.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverter.cs @@ -5,13 +5,20 @@ 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 { + public DotNetObjectReferenceJsonConverter(JSRuntime jsRuntime) + { + JSRuntime = jsRuntime; + } + private static JsonEncodedText DotNetObjectRefKey => DotNetDispatcher.DotNetObjectRefKey; - public override DotNetObjectRef Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + 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 throw new JsonException($"Required property {DotNetObjectRefKey} not found."); } - var value = (TValue)DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId); - return new DotNetObjectRef(dotNetObjectId, value); + var value = (DotNetObjectReference)JSRuntime.GetObjectReference(dotNetObjectId); + return value; } - public override void Write(Utf8JsonWriter writer, DotNetObjectRef value, JsonSerializerOptions options) + 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/DotNetObjectRefJsonConverterFactory.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs similarity index 76% rename from src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs index 5cfec5be9d..288bfdd090 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetObjectRefJsonConverterFactory.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/DotNetObjectReferenceJsonConverterFactory.cs @@ -5,13 +5,20 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; -namespace Microsoft.JSInterop +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(DotNetObjectRef<>); + return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(DotNetObjectReference<>); } public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions jsonSerializerOptions) @@ -20,7 +27,7 @@ namespace Microsoft.JSInterop 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/IDotNetObjectRef.cs b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/IDotNetObjectReference.cs similarity index 59% rename from src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs rename to src/JSInterop/Microsoft.JSInterop/src/Infrastructure/IDotNetObjectReference.cs index b082d0ce10..4b84f2bd0c 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IDotNetObjectRef.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Infrastructure/IDotNetObjectReference.cs @@ -3,9 +3,10 @@ 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 90% rename from src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntimeBase.cs rename to src/JSInterop/Microsoft.JSInterop/src/JSInProcessRuntime.cs index 7606f1865f..2b96bbbbb5 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. @@ -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/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..4dca7a5db3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -2,29 +2,234 @@ // 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.Diagnostics; +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 _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 _trackedRefsById = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _cancellationRegistrations = + new ConcurrentDictionary(); /// - /// Sets the current JS runtime to the supplied instance. - /// - /// This is intended for framework use. Developers should not normally need to call this method. + /// Initializes a new instance of . /// - /// The new current . - public static void SetCurrentJSRuntime(IJSRuntime instance) + protected JSRuntime() { - _currentJSRuntime.Value = instance - ?? throw new ArgumentNullException(nameof(instance)); + 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; } + + /// + /// 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, JsonSerializerOptions) : + 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 . + /// The . + protected internal abstract void EndInvokeDotNet( + DotNetInvocationInfo invocationInfo, + in DotNetInvocationResult invocationResult); + + 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, JsonSerializerOptions); + 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)); + } + } + + 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/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/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/DotNetObjectRefTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs deleted file mode 100644 index 112363869e..0000000000 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectRefTest.cs +++ /dev/null @@ -1,34 +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.Threading.Tasks; -using Xunit; -using static Microsoft.JSInterop.TestJSRuntime; - -namespace Microsoft.JSInterop -{ - public class DotNetObjectRefTest - { - [Fact] - public Task CanAccessValue() => WithJSRuntime(_ => - { - var obj = new object(); - Assert.Same(obj, DotNetObjectRef.Create(obj).Value); - }); - - [Fact] - public Task NotifiesAssociatedJsRuntimeOfDisposal() => WithJSRuntime(jsRuntime => - { - // Arrange - var objRef = DotNetObjectRef.Create(new object()); - - // Act - objRef.Dispose(); - - // Assert - var ex = Assert.Throws(() => jsRuntime.ObjectRefManager.FindDotNetObject(objRef.ObjectId)); - Assert.StartsWith("There is no tracked object with id '1'.", ex.Message); - }); - } -} diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs new file mode 100644 index 0000000000..95fad485a7 --- /dev/null +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceTest.cs @@ -0,0 +1,103 @@ +// 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 Xunit; + +namespace Microsoft.JSInterop +{ + public class DotNetObjectReferenceTest + { + [Fact] + public void CanAccessValue() + { + var obj = new object(); + Assert.Same(obj, DotNetObjectReference.Create(obj).Value); + } + + [Fact] + 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 + 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/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs similarity index 65% rename from src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs rename to src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetDispatcherTest.cs index 282aa0f364..7e82a47a89 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 { @@ -20,7 +20,7 @@ namespace Microsoft.JSInterop { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(" ", "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 { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke("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 var assemblyName = "Some.Fake.Assembly"; var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(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,54 +67,58 @@ namespace Microsoft.JSInterop { var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(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); } [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, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticVoid", default, 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, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticNonVoid", default, 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, new DotNetInvocationInfo(thisAssemblyName, nameof(SomePublicType.InvokableMethodWithoutCustomIdentifier), default, 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 = DotNetObjectRef.Create(arg3); + var objectRef = DotNetObjectReference.Create(arg3); jsRuntime.Invoke("unimportant", objectRef); // Arrange: Remaining args @@ -123,15 +127,15 @@ namespace Microsoft.JSInterop 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, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, 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,18 +146,19 @@ 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>(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 = DotNetObjectRef.Create(arg3); + var objectRef = DotNetObjectReference.Create(arg3); jsRuntime.Invoke("unimportant", objectRef); // Arrange: Remaining args @@ -162,218 +167,246 @@ namespace Microsoft.JSInterop 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, 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); - }); + } [Fact] - public Task CanInvokeInstanceVoidMethod() => WithJSRuntime(jsRuntime => + public void CanInvokeInstanceVoidMethod() { // Arrange: Track some instance + var jsRuntime = new TestJSRuntime(); var targetInstance = new SomePublicType(); - var objectRef = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(null, "InvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "InvokableInstanceVoid", 1, default), 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 = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); // Act - var resultJson = DotNetDispatcher.Invoke(null, "BaseClassInvokableInstanceVoid", 1, null); + var resultJson = DotNetDispatcher.Invoke(jsRuntime, new DotNetInvocationInfo(null, "BaseClassInvokableInstanceVoid", 1, default), null); // Assert Assert.Null(resultJson); Assert.True(targetInstance.DidInvokeMyBaseClassInvocableInstanceVoid); - }); + } [Fact] - public Task CannotUseDotNetObjectRefAfterDisposal() => 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(jsRuntime, new DotNetInvocationInfo(null, "__Dispose", objectRef.ObjectId, default), null); + + // Assert + Assert.True(objectRef.Disposed); + } + + [Fact] + 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 = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); objectRef.Dispose(); // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(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); - }); + } [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 = DotNetObjectRef.Create(targetInstance); + var objectRef = DotNetObjectReference.Create(targetInstance); jsRuntime.Invoke("unimportant", objectRef); - DotNetDispatcher.ReleaseDotNetObject(1); + objectRef.Dispose(); // Act/Assert var ex = Assert.Throws( - () => DotNetDispatcher.Invoke(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); - }); + } [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.EndInvoke(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, 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); - }); + } [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.EndInvoke(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert - var ex = await Assert.ThrowsAsync(() => task); + 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.EndInvoke(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.EndInvoke(argsJson); + DotNetDispatcher.EndInvokeJS(jsRuntime, argsJson); // Assert - var ex = await Assert.ThrowsAsync(() => task); + 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", - DotNetObjectRef.Create(targetInstance), - DotNetObjectRef.Create(arg2)); + DotNetObjectReference.Create(targetInstance), + DotNetObjectReference.Create(arg2)); var argsJson = "[\"myvalue\",{\"__dotNetObject\":2}]"; // Act - var resultJson = DotNetDispatcher.Invoke(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); - var resultDto = (TestDTO)jsRuntime.ObjectRefManager.FindDotNetObject(3); + 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, new DotNetInvocationInfo(thisAssemblyName, "InvocableStaticWithParams", default, 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 objectRef = DotNetObjectRef.Create(new TestDTO { IntVal = 4 }); + var jsRuntime = new TestJSRuntime(); + var objectRef = DotNetObjectReference.Create(new TestDTO { IntVal = 4 }); var argsJson = JsonSerializer.Serialize(new object[] { new TestDTO { StringVal = "Another string", IntVal = 456 }, new[] { 100, 200 }, objectRef, 7, - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act/Assert var ex = Assert.Throws(() => { - DotNetDispatcher.Invoke(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); - }); + } [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 = 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 @@ -381,112 +414,114 @@ namespace Microsoft.JSInterop { new TestDTO { IntVal = 1000, StringVal = "String via JSON" }, arg2Ref, - }, JsonSerializerOptionsProvider.Options); + }, jsRuntime.JsonSerializerOptions); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvoke(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); 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); - }); + } [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.BeginInvoke(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] - public Task CanInvokeAsyncThrowingMethod() => WithJSRuntime(async jsRuntime => + public async Task CanInvokeAsyncThrowingMethod() { // Arrange + var jsRuntime = new TestJSRuntime(); // Act var callId = "123"; var resultTask = jsRuntime.NextInvocationTask; - DotNetDispatcher.BeginInvoke(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] - 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.BeginInvoke(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] - 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.BeginInvoke(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 DotNetObjectRef 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] [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] @@ -495,7 +530,7 @@ namespace Microsoft.JSInterop 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] @@ -504,7 +539,7 @@ namespace Microsoft.JSInterop 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] @@ -514,7 +549,7 @@ namespace Microsoft.JSInterop 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); @@ -527,7 +562,7 @@ namespace Microsoft.JSInterop 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)); @@ -542,7 +577,7 @@ namespace Microsoft.JSInterop 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( @@ -559,92 +594,72 @@ namespace Microsoft.JSInterop 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.IsCompleted && task.Status == TaskStatus.RanToCompletion); + 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.IsCompleted && task.Status == TaskStatus.RanToCompletion); + 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.IsCompleted && task.Status == TaskStatus.RanToCompletion); + 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() { } @@ -671,7 +686,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 @@ -679,7 +694,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() @@ -701,7 +716,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 @@ -709,7 +724,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() @@ -718,7 +733,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; @@ -729,7 +744,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, @@ -775,7 +790,7 @@ namespace Microsoft.JSInterop } } - public class TestJSRuntime : JSInProcessRuntimeBase + public class TestJSRuntime : JSInProcessRuntime { private TaskCompletionSource _nextInvocationTcs = new TaskCompletionSource(); public Task NextInvocationTask => _nextInvocationTcs.Task; @@ -784,8 +799,7 @@ namespace Microsoft.JSInterop 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) { @@ -806,17 +820,10 @@ namespace Microsoft.JSInterop 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/DotNetObjectReferenceJsonConverterTest.cs b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs similarity index 52% rename from src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs rename to src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs index 18f3db55c1..8d055aea2c 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetObjectReferenceJsonConverterTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/Infrastructure/DotNetObjectReferenceJsonConverterTest.cs @@ -2,116 +2,118 @@ // 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.Tests +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 dotNetObjectRef = DotNetObjectRef.Create(new TestModel()); + 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 dotNetObjectRef = DotNetObjectRef.Create(new TestModel()); + 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 = DotNetObjectRef.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var dotNetObjectRef = DotNetObjectReference.Create(input); + 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 = DotNetObjectRef.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var dotNetObjectRef = DotNetObjectReference.Create(input); + 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 = DotNetObjectRef.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var dotNetObjectRef = DotNetObjectReference.Create(input); + 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. 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}}}]"; + 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 = DotNetObjectRef.Create(input); - var objectId = dotNetObjectRef.ObjectId; + var dotNetObjectRef = DotNetObjectReference.Create(input); + var objectId = JSRuntime.TrackObjectReference(dotNetObjectRef); var json = @$"{{ @@ -119,27 +121,27 @@ namespace Microsoft.JSInterop.Tests }}"; // 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 = DotNetObjectRef.Create(new TestModel()); + 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/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 74% rename from src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs rename to src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs index a8d551e94e..f42e0801a0 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSInProcessRuntimeTest.cs @@ -4,9 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.JSInterop.Infrastructure; using Xunit; -namespace Microsoft.JSInterop.Tests +namespace Microsoft.JSInterop { public class JSInProcessRuntimeBaseTest { @@ -18,7 +19,6 @@ namespace Microsoft.JSInterop.Tests { NextResultJson = "{\"intValue\":123,\"stringValue\":\"Hello\"}" }; - JSRuntime.SetCurrentJSRuntime(runtime); // Act var syncResult = runtime.Invoke("test identifier 1", "arg1", 123, true); @@ -36,19 +36,18 @@ namespace Microsoft.JSInterop.Tests { // Arrange var runtime = new TestJSInProcessRuntime { NextResultJson = null }; - 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 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 @@ -60,9 +59,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.GetObjectReference(1).Value); + Assert.Same(obj2, runtime.GetObjectReference(2).Value); + Assert.Same(obj3, runtime.GetObjectReference(3).Value); } [Fact] @@ -73,16 +72,15 @@ namespace Microsoft.JSInterop.Tests { NextResultJson = "[{\"__dotNetObject\":2},{\"__dotNetObject\":1}]" }; - JSRuntime.SetCurrentJSRuntime(runtime); var obj1 = new object(); 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 +93,7 @@ namespace Microsoft.JSInterop.Tests public string StringValue { get; set; } } - class TestJSInProcessRuntime : JSInProcessRuntimeBase + class TestJSInProcessRuntime : JSInProcessRuntime { public List InvokeCalls { get; set; } = new List(); @@ -116,8 +114,8 @@ namespace Microsoft.JSInterop.Tests 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/JSRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs deleted file mode 100644 index 2714886f9a..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)); - Assert.Same(obj1, runtime.ObjectRefManager.FindDotNetObject(2)); - Assert.Same(obj2, runtime.ObjectRefManager.FindDotNetObject(3)); - Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4)); - } - - [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..66e0033d2a 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -4,36 +4,388 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.JSInterop.Infrastructure; using Xunit; -namespace Microsoft.JSInterop.Tests +namespace Microsoft.JSInterop { public class JSRuntimeTest { [Fact] - public async Task CanHaveDistinctJSRuntimeInstancesInEachAsyncContext() + public void DispatchesAsyncCallsWithDistinctAsyncHandles() { - var tasks = Enumerable.Range(0, 20).Select(async _ => - { - var jsRuntime = new FakeJSRuntime(); - JSRuntime.SetCurrentJSRuntime(jsRuntime); - await Task.Delay(50).ConfigureAwait(false); - Assert.Same(jsRuntime, JSRuntime.Current); - }); + // Arrange + var runtime = new TestJSRuntime(); - await Task.WhenAll(tasks); - Assert.Null(JSRuntime.Current); + // 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); + }); } - private class FakeJSRuntime : IJSRuntime + [Fact] + public async Task InvokeAsync_CancelsAsyncTask_AfterDefaultTimeout() { - public Task InvokeAsync(string identifier, params object[] args) - => throw new NotImplementedException(); + // Arrange + var runtime = new TestJSRuntime(); + runtime.DefaultTimeout = TimeSpan.FromSeconds(1); - public Task InvokeAsync(string identifier, IEnumerable args, CancellationToken cancellationToken = default) => - throw new NotImplementedException(); + // 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(); + 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\":2},\"obj3\":{\"__dotNetObject\":3},\"obj1SameRef\":{\"__dotNetObject\":1},\"obj1DifferentRef\":{\"__dotNetObject\":4}}]", call.ArgsJson); + + // Assert: Objects were tracked + Assert.Same(obj1Ref, runtime.GetObjectReference(1)); + Assert.Same(obj1, obj1Ref.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] + 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(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 = (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(invocation, result); + + // 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(DotNetInvocationInfo invocationInfo, in DotNetInvocationResult invocationResult) + { + var resultOrError = invocationResult.Success ? invocationResult.Result : invocationResult.Exception; + if (OnDotNetException != null && !invocationResult.Success) + { + resultOrError = OnDotNetException(invocationInfo); + } + + EndInvokeDotNetCalls.Add(new EndInvokeDotNetArgs + { + CallId = invocationInfo.CallId, + Success = invocationResult.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..db9c5ddd36 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/TestJSRuntime.cs @@ -2,31 +2,20 @@ // 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 Microsoft.JSInterop.Infrastructure; namespace Microsoft.JSInterop { - internal class TestJSRuntime : JSRuntimeBase + internal class TestJSRuntime : JSRuntime { protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson) { 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(); } - - 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 5299370576..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 @@ -3,11 +3,12 @@ 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) { } - 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; } 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 e65df172f8..654263a123 100644 --- a/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs +++ b/src/JSInterop/Mono.WebAssembly.Interop/src/MonoWebAssemblyJSRuntime.cs @@ -2,9 +2,9 @@ // 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; using WebAssembly.JSInterop; namespace Mono.WebAssembly.Interop @@ -13,8 +13,27 @@ 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 { + /// + /// 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) { @@ -33,11 +52,14 @@ 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); + { + 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) - => DotNetDispatcher.EndInvoke(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) @@ -58,32 +80,20 @@ namespace Mono.WebAssembly.Interop assemblyName = assemblyNameOrDotNetObjectId; } - DotNetDispatcher.BeginInvoke(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 }, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var args = JsonSerializer.Serialize(new[] { callInfo.CallId, dispatchResult.Success, resultOrError }, JsonSerializerOptions); BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args); } 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