Merge branch 'master' into merge/release/3.0-preview7-to-master\n\nCommit migrated from 576150b007
This commit is contained in:
commit
ddf3cb66f1
|
|
@ -147,7 +147,7 @@ module DotNet {
|
|||
* @param identifier Identifies the globally-reachable function to be returned.
|
||||
* @returns A Function instance.
|
||||
*/
|
||||
findJSFunction,
|
||||
findJSFunction, // Note that this is used by the JS interop code inside Mono WebAssembly itself
|
||||
|
||||
/**
|
||||
* Invokes the specified synchronous JavaScript function.
|
||||
|
|
@ -227,8 +227,10 @@ module DotNet {
|
|||
|
||||
let result: any = window;
|
||||
let resultIdentifier = 'window';
|
||||
let lastSegmentValue: any;
|
||||
identifier.split('.').forEach(segment => {
|
||||
if (segment in result) {
|
||||
lastSegmentValue = result;
|
||||
result = result[segment];
|
||||
resultIdentifier += '.' + segment;
|
||||
} else {
|
||||
|
|
@ -237,6 +239,8 @@ module DotNet {
|
|||
});
|
||||
|
||||
if (result instanceof Function) {
|
||||
result = result.bind(lastSegmentValue);
|
||||
cachedJSFunctions[identifier] = result;
|
||||
return result;
|
||||
} else {
|
||||
throw new Error(`The value '${resultIdentifier}' is not a function.`);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -75,19 +75,20 @@ namespace Microsoft.JSInterop
|
|||
// code has to implement its own way of returning async results.
|
||||
var jsRuntimeBaseInstance = (JSRuntimeBase)JSRuntime.Current;
|
||||
|
||||
var targetInstance = (object)null;
|
||||
if (dotNetObjectId != default)
|
||||
{
|
||||
targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
|
||||
}
|
||||
|
||||
// Using ExceptionDispatchInfo here throughout because we want to always preserve
|
||||
// original stack traces.
|
||||
object syncResult = null;
|
||||
ExceptionDispatchInfo syncException = null;
|
||||
object targetInstance = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (dotNetObjectId != default)
|
||||
{
|
||||
targetInstance = DotNetObjectRefManager.Current.FindDotNetObject(dotNetObjectId);
|
||||
}
|
||||
|
||||
syncResult = InvokeSynchronously(assemblyName, methodIdentifier, targetInstance, argsJson);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -103,7 +104,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 +115,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);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ namespace Microsoft.JSInterop
|
|||
{
|
||||
public static readonly JsonSerializerOptions Options = new JsonSerializerOptions
|
||||
{
|
||||
MaxDepth = 32,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,6 +381,23 @@ namespace Microsoft.JSInterop.Tests
|
|||
Assert.Contains("JsonReaderException: '<' is an invalid start of a value.", result[2].GetString());
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public Task BeginInvoke_ThrowsWithInvalid_DotNetObjectRef() => WithJSRuntime(jsRuntime =>
|
||||
{
|
||||
// Arrange
|
||||
var callId = "123";
|
||||
var resultTask = jsRuntime.NextInvocationTask;
|
||||
DotNetDispatcher.BeginInvoke(callId, null, "InvokableInstanceVoid", 1, null);
|
||||
|
||||
// Assert
|
||||
using var jsonDocument = JsonDocument.Parse(jsRuntime.LastInvocationArgsJson);
|
||||
var result = jsonDocument.RootElement;
|
||||
Assert.Equal(callId, result[0].GetString());
|
||||
Assert.False(result[1].GetBoolean()); // Fails
|
||||
|
||||
Assert.StartsWith("System.ArgumentException: There is no tracked object with id '1'. Perhaps the DotNetObjectRef instance was already disposed.", result[2].GetString());
|
||||
});
|
||||
|
||||
Task WithJSRuntime(Action<TestJSRuntime> testCode)
|
||||
{
|
||||
return WithJSRuntime(jsRuntime =>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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.
|
||||
// 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;
|
||||
|
||||
|
|
@ -56,6 +56,10 @@ namespace Microsoft.Extensions.Localization
|
|||
SearchedLocation = searchedLocation;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts the <see cref="LocalizedString"/> to a <see cref="string"/>.
|
||||
/// </summary>
|
||||
/// <param name="localizedString">The string to be implicitly converted.</param>
|
||||
public static implicit operator string(LocalizedString localizedString)
|
||||
{
|
||||
return localizedString?.Value;
|
||||
|
|
@ -87,4 +91,4 @@ namespace Microsoft.Extensions.Localization
|
|||
/// <returns>The actual string.</returns>
|
||||
public override string ToString() => Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
// 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.
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.Extensions.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for operating on <see cref="IStringLocalizer" /> instances.
|
||||
/// </summary>
|
||||
public static class StringLocalizerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@ namespace Microsoft.Extensions.Localization
|
|||
/// </summary>
|
||||
public class LocalizationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LocalizationOptions" />.
|
||||
/// </summary>
|
||||
public LocalizationOptions()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// The relative path under application root where resource files are located.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ namespace Microsoft.Extensions.Localization
|
|||
/// <summary>
|
||||
/// Returns all strings in the specified culture.
|
||||
/// </summary>
|
||||
/// <param name="includeParentCultures"></param>
|
||||
/// <param name="includeParentCultures">Whether to include parent cultures in the search for a resource.</param>
|
||||
/// <param name="culture">The <see cref="CultureInfo"/> to get strings for.</param>
|
||||
/// <returns>The strings.</returns>
|
||||
protected IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures, CultureInfo culture)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// 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.
|
||||
// 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;
|
||||
|
|
@ -14,6 +14,13 @@ namespace Microsoft.Extensions.Localization
|
|||
{
|
||||
private readonly ConcurrentDictionary<string, IList<string>> _cache = new ConcurrentDictionary<string, IList<string>>();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ResourceNamesCache" />
|
||||
/// </summary>
|
||||
public ResourceNamesCache()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<string> GetOrAdd(string name, Func<string, IList<string>> valueFactory)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ using System.Threading;
|
|||
|
||||
namespace Microsoft.Extensions.ObjectPool
|
||||
{
|
||||
/// <summary>
|
||||
/// Default implementation of <see cref="ObjectPool{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to pool objects for.</typeparam>
|
||||
/// <remarks>This implementation keeps a cache of retained objects. This means that if objects are returned when the pool has already reached "maximumRetained" objects they will be available to be Garbage Collected.</remarks>
|
||||
public class DefaultObjectPool<T> : ObjectPool<T> where T : class
|
||||
{
|
||||
private protected readonly ObjectWrapper[] _items;
|
||||
|
|
@ -18,11 +23,20 @@ namespace Microsoft.Extensions.ObjectPool
|
|||
// This class was introduced in 2.1 to avoid the interface call where possible
|
||||
private protected readonly PooledObjectPolicy<T> _fastPolicy;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="DefaultObjectPool{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="policy">The pooling policy to use.</param>
|
||||
public DefaultObjectPool(IPooledObjectPolicy<T> policy)
|
||||
: this(policy, Environment.ProcessorCount * 2)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of <see cref="DefaultObjectPool{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="policy">The pooling policy to use.</param>
|
||||
/// <param name="maximumRetained">The maximum number of objects to retain in the pool.</param>
|
||||
public DefaultObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained)
|
||||
{
|
||||
_policy = policy ?? throw new ArgumentNullException(nameof(policy));
|
||||
|
|
|
|||
|
|
@ -5,10 +5,17 @@ using System;
|
|||
|
||||
namespace Microsoft.Extensions.ObjectPool
|
||||
{
|
||||
/// <summary>
|
||||
/// The default <see cref="ObjectPoolProvider"/>.
|
||||
/// </summary>
|
||||
public class DefaultObjectPoolProvider : ObjectPoolProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum number of objects to retain in the pool.
|
||||
/// </summary>
|
||||
public int MaximumRetained { get; set; } = Environment.ProcessorCount * 2;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy)
|
||||
{
|
||||
if (policy == null)
|
||||
|
|
|
|||
|
|
@ -3,10 +3,23 @@
|
|||
|
||||
namespace Microsoft.Extensions.ObjectPool
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a policy for managing pooled objects.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of object which is being pooled.</typeparam>
|
||||
public interface IPooledObjectPolicy<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <returns>The <typeparamref name="T"/> which was created.</returns>
|
||||
T Create();
|
||||
|
||||
/// <summary>
|
||||
/// Runs some processing when an object was returned to the pool. Can be used to reset the state of an object and indicate if the object should be returned to the pool.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to return to the pool.</param>
|
||||
/// <returns><code>true</code> if the object should be returned to the pool. <code>false</code> if it's not possible/desirable for the pool to keep the object.</returns>
|
||||
bool Return(T obj);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,15 +3,31 @@
|
|||
|
||||
namespace Microsoft.Extensions.ObjectPool
|
||||
{
|
||||
/// <summary>
|
||||
/// A pool of objects.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of objects to pool.</typeparam>
|
||||
public abstract class ObjectPool<T> where T : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an object from the pool if one is available, otherwise creates one.
|
||||
/// </summary>
|
||||
/// <returns>A <typeparamref name="T"/>.</returns>
|
||||
public abstract T Get();
|
||||
|
||||
/// <summary>
|
||||
/// Return an object to the pool.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to add to the pool.</param>
|
||||
public abstract void Return(T obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Methods for creating <see cref="ObjectPool{T}"/> instances.
|
||||
/// </summary>
|
||||
public static class ObjectPool
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public static ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy = null) where T : class, new()
|
||||
{
|
||||
var provider = new DefaultObjectPoolProvider();
|
||||
|
|
|
|||
|
|
@ -3,13 +3,24 @@
|
|||
|
||||
namespace Microsoft.Extensions.ObjectPool
|
||||
{
|
||||
/// <summary>
|
||||
/// A provider of <see cref="ObjectPool{T}"/> instances.
|
||||
/// </summary>
|
||||
public abstract class ObjectPoolProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ObjectPool"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to create a pool for.</typeparam>
|
||||
public ObjectPool<T> Create<T>() where T : class, new()
|
||||
{
|
||||
return Create<T>(new DefaultPooledObjectPolicy<T>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="ObjectPool"/> with the given <see cref="IPooledObjectPolicy{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to create a pool for.</typeparam>
|
||||
public abstract ObjectPool<T> Create<T>(IPooledObjectPolicy<T> policy) where T : class;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ namespace Microsoft.Extensions.CommandLineUtils
|
|||
/// <remarks>
|
||||
/// See https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
|
||||
/// </remarks>
|
||||
/// <param name="args"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="args">The arguments to concatenate.</param>
|
||||
/// <returns>The escaped arguments, concatenated.</returns>
|
||||
public static string EscapeAndConcatenate(IEnumerable<string> args)
|
||||
=> string.Join(" ", args.Select(EscapeSingleArg));
|
||||
|
||||
|
|
@ -104,6 +104,6 @@ namespace Microsoft.Extensions.CommandLineUtils
|
|||
}
|
||||
|
||||
private static bool ContainsWhitespace(string argument)
|
||||
=> argument.IndexOfAny(new [] { ' ', '\t', '\n' }) >= 0;
|
||||
=> argument.IndexOfAny(new[] { ' ', '\t', '\n' }) >= 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue