Merge branch 'master' into merge/release/3.0-preview7-to-master\n\nCommit migrated from 609186745f
This commit is contained in:
commit
8402271cf3
|
|
@ -32,6 +32,7 @@ namespace Microsoft.JSInterop
|
|||
}
|
||||
public partial interface IJSRuntime
|
||||
{
|
||||
System.Threading.Tasks.Task<TValue> InvokeAsync<TValue>(string identifier, System.Collections.Generic.IEnumerable<object> args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
|
||||
System.Threading.Tasks.Task<TValue> InvokeAsync<TValue>(string identifier, params object[] args);
|
||||
}
|
||||
public partial class JSException : System.Exception
|
||||
|
|
@ -59,8 +60,11 @@ namespace Microsoft.JSInterop
|
|||
public abstract partial class JSRuntimeBase : Microsoft.JSInterop.IJSRuntime
|
||||
{
|
||||
protected JSRuntimeBase() { }
|
||||
protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson);
|
||||
protected System.TimeSpan? DefaultAsyncTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||
protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson);
|
||||
public System.Threading.Tasks.Task<T> InvokeAsync<T>(string identifier, System.Collections.Generic.IEnumerable<object> args, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
|
||||
public System.Threading.Tasks.Task<T> InvokeAsync<T>(string identifier, params object[] args) { throw null; }
|
||||
protected virtual object OnDotNetInvocationException(System.Exception exception, string assemblyName, string methodIdentifier) { throw null; }
|
||||
}
|
||||
}
|
||||
namespace Microsoft.JSInterop.Internal
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ namespace Microsoft.JSInterop
|
|||
else if (syncException != null)
|
||||
{
|
||||
// Threw synchronously, let's respond.
|
||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException);
|
||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier);
|
||||
}
|
||||
else if (syncResult is Task task)
|
||||
{
|
||||
|
|
@ -114,16 +114,17 @@ namespace Microsoft.JSInterop
|
|||
if (t.Exception != null)
|
||||
{
|
||||
var exception = t.Exception.GetBaseException();
|
||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception));
|
||||
|
||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier);
|
||||
}
|
||||
|
||||
var result = TaskGenericsUtil.GetTaskResult(task);
|
||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result);
|
||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier);
|
||||
}, TaskScheduler.Current);
|
||||
}
|
||||
else
|
||||
{
|
||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult);
|
||||
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.JSInterop
|
||||
|
|
@ -19,5 +20,15 @@ namespace Microsoft.JSInterop
|
|||
/// <param name="args">JSON-serializable arguments.</param>
|
||||
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
|
||||
Task<TValue> InvokeAsync<TValue>(string identifier, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified JavaScript function asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The JSON-serializable return type.</typeparam>
|
||||
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
|
||||
/// <param name="args">JSON-serializable arguments.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to signal the cancellation of the operation.</param>
|
||||
/// <returns>An instance of <typeparamref name="TValue"/> obtained by JSON-deserializing the return value.</returns>
|
||||
Task<TValue> InvokeAsync<TValue>(string identifier, IEnumerable<object> args, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
|
@ -20,8 +22,71 @@ namespace Microsoft.JSInterop
|
|||
private readonly ConcurrentDictionary<long, object> _pendingTasks
|
||||
= new ConcurrentDictionary<long, object>();
|
||||
|
||||
private readonly ConcurrentDictionary<long, CancellationTokenRegistration> _cancellationRegistrations =
|
||||
new ConcurrentDictionary<long, CancellationTokenRegistration>();
|
||||
|
||||
internal DotNetObjectRefManager ObjectRefManager { get; } = new DotNetObjectRefManager();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default timeout for asynchronous JavaScript calls.
|
||||
/// </summary>
|
||||
protected TimeSpan? DefaultAsyncTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified JavaScript function asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The JSON-serializable return type.</typeparam>
|
||||
/// <param name="identifier">An identifier for the function to invoke. For example, the value <code>"someScope.someFunction"</code> will invoke the function <code>window.someScope.someFunction</code>.</param>
|
||||
/// <param name="args">JSON-serializable arguments.</param>
|
||||
/// <param name="cancellationToken">A cancellation token to signal the cancellation of the operation.</param>
|
||||
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
|
||||
public Task<T> InvokeAsync<T>(string identifier, IEnumerable<object> args, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var taskId = Interlocked.Increment(ref _nextPendingTaskId);
|
||||
var tcs = new TaskCompletionSource<T>(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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the specified JavaScript function asynchronously.
|
||||
/// </summary>
|
||||
|
|
@ -31,49 +96,58 @@ namespace Microsoft.JSInterop
|
|||
/// <returns>An instance of <typeparamref name="T"/> obtained by JSON-deserializing the return value.</returns>
|
||||
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
|
||||
{
|
||||
// We might consider also adding a default timeout here in case we don't want to
|
||||
// risk a memory leak in the scenario where the JS-side code is failing to complete
|
||||
// the operation.
|
||||
|
||||
var taskId = Interlocked.Increment(ref _nextPendingTaskId);
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
_pendingTasks[taskId] = tcs;
|
||||
|
||||
try
|
||||
if (!DefaultAsyncTimeout.HasValue)
|
||||
{
|
||||
var argsJson = args?.Length > 0 ?
|
||||
JsonSerializer.Serialize(args, JsonSerializerOptionsProvider.Options) :
|
||||
null;
|
||||
BeginInvokeJS(taskId, identifier, argsJson);
|
||||
return tcs.Task;
|
||||
return InvokeAsync<T>(identifier, args, default);
|
||||
}
|
||||
catch
|
||||
else
|
||||
{
|
||||
_pendingTasks.TryRemove(taskId, out _);
|
||||
throw;
|
||||
return InvokeWithDefaultCancellation<T>(identifier, args);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<T> InvokeWithDefaultCancellation<T>(string identifier, IEnumerable<object> args)
|
||||
{
|
||||
using (var cts = new CancellationTokenSource(DefaultAsyncTimeout.Value))
|
||||
{
|
||||
// We need to await here due to the using
|
||||
return await InvokeAsync<T>(identifier, args, cts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins an asynchronous function invocation.
|
||||
/// </summary>
|
||||
/// <param name="asyncHandle">The identifier for the function invocation, or zero if no async callback is required.</param>
|
||||
/// <param name="taskId">The identifier for the function invocation, or zero if no async callback is required.</param>
|
||||
/// <param name="identifier">The identifier for the function to invoke.</param>
|
||||
/// <param name="argsJson">A JSON representation of the arguments.</param>
|
||||
protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson);
|
||||
protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson);
|
||||
|
||||
internal void EndInvokeDotNet(string callId, bool success, object resultOrException)
|
||||
/// <summary>
|
||||
/// Allows derived classes to configure the information about an exception in a JS interop call that gets sent to JavaScript.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This callback can be used in remote JS interop scenarios to sanitize exceptions that happen on the server to avoid disclosing
|
||||
/// sensitive information to remote browser clients.
|
||||
/// </remarks>
|
||||
/// <param name="exception">The exception that occurred.</param>
|
||||
/// <param name="assemblyName">The assembly for the invoked .NET method.</param>
|
||||
/// <param name="methodIdentifier">The identifier for the invoked .NET method.</param>
|
||||
/// <returns>An object containing information about the exception.</returns>
|
||||
protected virtual object OnDotNetInvocationException(Exception exception, string assemblyName, string methodIdentifier) => exception.ToString();
|
||||
|
||||
internal void EndInvokeDotNet(string callId, bool success, object resultOrException, string assemblyName, string methodIdentifier)
|
||||
{
|
||||
// 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 && resultOrException is Exception)
|
||||
if (!success && resultOrException is Exception ex)
|
||||
{
|
||||
resultOrException = resultOrException.ToString();
|
||||
resultOrException = OnDotNetInvocationException(ex, assemblyName, methodIdentifier);
|
||||
}
|
||||
else if (!success && resultOrException is ExceptionDispatchInfo edi)
|
||||
{
|
||||
resultOrException = edi.SourceException.ToString();
|
||||
resultOrException = OnDotNetInvocationException(edi.SourceException, assemblyName, methodIdentifier);
|
||||
}
|
||||
|
||||
// We pass 0 as the async handle because we don't want the JS-side code to
|
||||
|
|
@ -82,15 +156,19 @@ namespace Microsoft.JSInterop
|
|||
BeginInvokeJS(0, "DotNet.jsCallDispatcher.endInvokeDotNetFromJS", args);
|
||||
}
|
||||
|
||||
internal void EndInvokeJS(long asyncHandle, bool succeeded, JSAsyncCallResult asyncCallResult)
|
||||
internal void EndInvokeJS(long taskId, bool succeeded, JSAsyncCallResult asyncCallResult)
|
||||
{
|
||||
using (asyncCallResult?.JsonDocument)
|
||||
{
|
||||
if (!_pendingTasks.TryRemove(asyncHandle, out var tcs))
|
||||
if (!_pendingTasks.TryRemove(taskId, out var tcs))
|
||||
{
|
||||
throw new ArgumentException($"There is no pending task with handle '{asyncHandle}'.");
|
||||
// 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);
|
||||
|
||||
if (succeeded)
|
||||
{
|
||||
var resultType = TaskGenericsUtil.GetTaskCompletionSourceResultType(tcs);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop.Internal;
|
||||
using Xunit;
|
||||
|
|
@ -38,6 +39,69 @@ namespace Microsoft.JSInterop.Tests
|
|||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_CancelsAsyncTask_AfterDefaultTimeout()
|
||||
{
|
||||
// Arrange
|
||||
var runtime = new TestJSRuntime();
|
||||
runtime.DefaultTimeout = TimeSpan.FromSeconds(1);
|
||||
|
||||
// Act
|
||||
var task = runtime.InvokeAsync<object>("test identifier 1", "arg1", 123, true);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(async () => await task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_CompletesSuccessfullyBeforeTimeout()
|
||||
{
|
||||
// Arrange
|
||||
var runtime = new TestJSRuntime();
|
||||
runtime.DefaultTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
// Act
|
||||
var task = runtime.InvokeAsync<object>("test identifier 1", "arg1", 123, true);
|
||||
runtime.EndInvokeJS(2, succeeded: true, null);
|
||||
|
||||
// Assert
|
||||
await task;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeAsync_CancelsAsyncTasksWhenCancellationTokenFires()
|
||||
{
|
||||
// Arrange
|
||||
using var cts = new CancellationTokenSource();
|
||||
var runtime = new TestJSRuntime();
|
||||
|
||||
// Act
|
||||
var task = runtime.InvokeAsync<object>("test identifier 1", new object[] { "arg1", 123, true }, cts.Token);
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(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<object>("test identifier 1", new object[] { "arg1", 123, true }, cts.Token);
|
||||
|
||||
cts.Cancel();
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<TaskCanceledException>(async () => await task);
|
||||
Assert.Empty(runtime.BeginInvokeCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCompleteAsyncCallsAsSuccess()
|
||||
{
|
||||
|
|
@ -115,21 +179,19 @@ namespace Microsoft.JSInterop.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void CannotCompleteSameAsyncCallMoreThanOnce()
|
||||
public async Task CompletingSameAsyncCallMoreThanOnce_IgnoresSecondResultAsync()
|
||||
{
|
||||
// Arrange
|
||||
var runtime = new TestJSRuntime();
|
||||
|
||||
// Act/Assert
|
||||
runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
|
||||
var task = runtime.InvokeAsync<string>("test identifier", Array.Empty<object>());
|
||||
var asyncHandle = runtime.BeginInvokeCalls[0].AsyncHandle;
|
||||
runtime.OnEndInvoke(asyncHandle, true, null);
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
// Second "end invoke" will fail
|
||||
runtime.OnEndInvoke(asyncHandle, true, null);
|
||||
});
|
||||
Assert.Equal($"There is no pending task with handle '{asyncHandle}'.", ex.Message);
|
||||
runtime.OnEndInvoke(asyncHandle, true, new JSAsyncCallResult(JsonDocument.Parse("{}"), JsonDocument.Parse("{\"Message\": \"Some data\"}").RootElement.GetProperty("Message")));
|
||||
runtime.OnEndInvoke(asyncHandle, false, new JSAsyncCallResult(null, JsonDocument.Parse("{\"Message\": \"Exception\"}").RootElement.GetProperty("Message")));
|
||||
|
||||
var result = await task;
|
||||
Assert.Equal("Some data", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -168,10 +230,50 @@ namespace Microsoft.JSInterop.Tests
|
|||
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");
|
||||
|
||||
// Assert
|
||||
var call = runtime.BeginInvokeCalls.Single();
|
||||
Assert.Equal(0, call.AsyncHandle);
|
||||
Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", call.Identifier);
|
||||
Assert.Equal($"[\"0\",false,{{\"message\":\"{expectedMessage.Replace("'", "\\u0027")}\"}}]", call.ArgsJson);
|
||||
}
|
||||
|
||||
private class JSError
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
class TestJSRuntime : JSRuntimeBase
|
||||
{
|
||||
public List<BeginInvokeAsyncArgs> BeginInvokeCalls = new List<BeginInvokeAsyncArgs>();
|
||||
|
||||
public TimeSpan? DefaultTimeout
|
||||
{
|
||||
set
|
||||
{
|
||||
base.DefaultAsyncTimeout = value;
|
||||
}
|
||||
}
|
||||
|
||||
public class BeginInvokeAsyncArgs
|
||||
{
|
||||
public long AsyncHandle { get; set; }
|
||||
|
|
@ -179,6 +281,18 @@ namespace Microsoft.JSInterop.Tests
|
|||
public string ArgsJson { get; set; }
|
||||
}
|
||||
|
||||
public Func<Exception, string, string, object> OnDotNetException { get; set; }
|
||||
|
||||
protected override object OnDotNetInvocationException(Exception exception, string assemblyName, string methodName)
|
||||
{
|
||||
if (OnDotNetException != null)
|
||||
{
|
||||
return OnDotNetException(exception, assemblyName, methodName);
|
||||
}
|
||||
|
||||
return base.OnDotNetInvocationException(exception, assemblyName, methodName);
|
||||
}
|
||||
|
||||
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
|
||||
{
|
||||
BeginInvokeCalls.Add(new BeginInvokeAsyncArgs
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -29,6 +31,9 @@ namespace Microsoft.JSInterop.Tests
|
|||
{
|
||||
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public Task<TValue> InvokeAsync<TValue>(string identifier, IEnumerable<object> args, CancellationToken cancellationToken = default) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue