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 5386b92bb5..27d22b0536 100644 --- a/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts +++ b/src/JSInterop/Microsoft.JSInterop.JS/src/src/Microsoft.JSInterop.ts @@ -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.`); 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 c2884dc64a..ad95b7342b 100644 --- a/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs +++ b/src/JSInterop/Microsoft.JSInterop/ref/Microsoft.JSInterop.netstandard2.0.cs @@ -32,6 +32,7 @@ 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); } 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 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 virtual object OnDotNetInvocationException(System.Exception exception, string assemblyName, string methodIdentifier) { throw null; } } } namespace Microsoft.JSInterop.Internal diff --git a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs index 360efae5d1..952a6061ab 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/DotNetDispatcher.cs @@ -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); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs index 97713bb3c1..d7ee372a73 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSRuntime.cs @@ -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 /// JSON-serializable arguments. /// An instance of obtained by JSON-deserializing the return value. Task InvokeAsync(string identifier, params 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. + /// 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); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs index 50fd7e2839..562a6a27ba 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntimeBase.cs @@ -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 _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. /// @@ -31,49 +96,58 @@ namespace Microsoft.JSInterop /// An instance of obtained by JSON-deserializing the return value. public Task InvokeAsync(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(); - _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(identifier, args, default); } - catch + else { - _pendingTasks.TryRemove(taskId, out _); - throw; + 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 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 asyncHandle, string identifier, string argsJson); + protected abstract void BeginInvokeJS(long taskId, string identifier, string argsJson); - internal void EndInvokeDotNet(string callId, bool success, object resultOrException) + /// + /// Allows derived classes to configure the information about an exception in a JS interop call that gets sent to JavaScript. + /// + /// + /// 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. + /// + /// The exception that occurred. + /// The assembly for the invoked .NET method. + /// The identifier for the invoked .NET method. + /// An object containing information about the exception. + 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); diff --git a/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs b/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs index fb299aaf02..62244270e3 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JsonSerializerOptionsProvider.cs @@ -9,7 +9,9 @@ namespace Microsoft.JSInterop { public static readonly JsonSerializerOptions Options = new JsonSerializerOptions { + MaxDepth = 32, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, }; } } diff --git a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs index 4e8733e067..9e8a7b672e 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/DotNetDispatcherTest.cs @@ -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 testCode) { return WithJSRuntime(jsRuntime => diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs index afbe5d0595..3ad78c303f 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeBaseTest.cs @@ -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("test identifier 1", "arg1", 123, true); + + // Assert + await Assert.ThrowsAsync(async () => await task); + } + + [Fact] + public async Task InvokeAsync_CompletesSuccessfullyBeforeTimeout() + { + // Arrange + var runtime = new TestJSRuntime(); + runtime.DefaultTimeout = TimeSpan.FromSeconds(10); + + // Act + var task = runtime.InvokeAsync("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("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() { @@ -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("test identifier", Array.Empty()); + var task = runtime.InvokeAsync("test identifier", Array.Empty()); var asyncHandle = runtime.BeginInvokeCalls[0].AsyncHandle; - runtime.OnEndInvoke(asyncHandle, true, null); - var ex = Assert.Throws(() => - { - // 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 BeginInvokeCalls = new List(); + 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 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 diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index ca8c96df60..f2fab5d741 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -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 InvokeAsync(string identifier, params object[] args) => throw new NotImplementedException(); + + public Task InvokeAsync(string identifier, IEnumerable args, CancellationToken cancellationToken = default) => + throw new NotImplementedException(); } } } diff --git a/src/Localization/Abstractions/src/LocalizedString.cs b/src/Localization/Abstractions/src/LocalizedString.cs index 6556da40a0..7cac58d16a 100644 --- a/src/Localization/Abstractions/src/LocalizedString.cs +++ b/src/Localization/Abstractions/src/LocalizedString.cs @@ -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; } + /// + /// Implicitly converts the to a . + /// + /// The string to be implicitly converted. public static implicit operator string(LocalizedString localizedString) { return localizedString?.Value; @@ -87,4 +91,4 @@ namespace Microsoft.Extensions.Localization /// The actual string. public override string ToString() => Value; } -} \ No newline at end of file +} diff --git a/src/Localization/Abstractions/src/StringLocalizerExtensions.cs b/src/Localization/Abstractions/src/StringLocalizerExtensions.cs index bde47f74f3..2e19db7c18 100644 --- a/src/Localization/Abstractions/src/StringLocalizerExtensions.cs +++ b/src/Localization/Abstractions/src/StringLocalizerExtensions.cs @@ -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 { + /// + /// Extension methods for operating on instances. + /// public static class StringLocalizerExtensions { /// diff --git a/src/Localization/Localization/src/LocalizationOptions.cs b/src/Localization/Localization/src/LocalizationOptions.cs index 1b7408fe67..1c0b82d210 100644 --- a/src/Localization/Localization/src/LocalizationOptions.cs +++ b/src/Localization/Localization/src/LocalizationOptions.cs @@ -8,6 +8,12 @@ namespace Microsoft.Extensions.Localization /// public class LocalizationOptions { + /// + /// Creates a new . + /// + public LocalizationOptions() + { } + /// /// The relative path under application root where resource files are located. /// diff --git a/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs b/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs index 90f8e077b4..a8321fca0a 100644 --- a/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs +++ b/src/Localization/Localization/src/ResourceManagerStringLocalizer.cs @@ -177,7 +177,7 @@ namespace Microsoft.Extensions.Localization /// /// Returns all strings in the specified culture. /// - /// + /// Whether to include parent cultures in the search for a resource. /// The to get strings for. /// The strings. protected IEnumerable GetAllStrings(bool includeParentCultures, CultureInfo culture) diff --git a/src/Localization/Localization/src/ResourceNamesCache.cs b/src/Localization/Localization/src/ResourceNamesCache.cs index 86e94c102a..72b7986d9f 100644 --- a/src/Localization/Localization/src/ResourceNamesCache.cs +++ b/src/Localization/Localization/src/ResourceNamesCache.cs @@ -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> _cache = new ConcurrentDictionary>(); + /// + /// Creates a new + /// + public ResourceNamesCache() + { + } + /// public IList GetOrAdd(string name, Func> valueFactory) { diff --git a/src/ObjectPool/src/DefaultObjectPool.cs b/src/ObjectPool/src/DefaultObjectPool.cs index 070508a014..f5627b7898 100644 --- a/src/ObjectPool/src/DefaultObjectPool.cs +++ b/src/ObjectPool/src/DefaultObjectPool.cs @@ -8,6 +8,11 @@ using System.Threading; namespace Microsoft.Extensions.ObjectPool { + /// + /// Default implementation of . + /// + /// The type to pool objects for. + /// 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. public class DefaultObjectPool : ObjectPool 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 _fastPolicy; + /// + /// Creates an instance of . + /// + /// The pooling policy to use. public DefaultObjectPool(IPooledObjectPolicy policy) : this(policy, Environment.ProcessorCount * 2) { } + /// + /// Creates an instance of . + /// + /// The pooling policy to use. + /// The maximum number of objects to retain in the pool. public DefaultObjectPool(IPooledObjectPolicy policy, int maximumRetained) { _policy = policy ?? throw new ArgumentNullException(nameof(policy)); diff --git a/src/ObjectPool/src/DefaultObjectPoolProvider.cs b/src/ObjectPool/src/DefaultObjectPoolProvider.cs index 2e7767ab35..b37a946d6d 100644 --- a/src/ObjectPool/src/DefaultObjectPoolProvider.cs +++ b/src/ObjectPool/src/DefaultObjectPoolProvider.cs @@ -5,10 +5,17 @@ using System; namespace Microsoft.Extensions.ObjectPool { + /// + /// The default . + /// public class DefaultObjectPoolProvider : ObjectPoolProvider { + /// + /// The maximum number of objects to retain in the pool. + /// public int MaximumRetained { get; set; } = Environment.ProcessorCount * 2; + /// public override ObjectPool Create(IPooledObjectPolicy policy) { if (policy == null) diff --git a/src/ObjectPool/src/IPooledObjectPolicy.cs b/src/ObjectPool/src/IPooledObjectPolicy.cs index 54611bad30..7519398030 100644 --- a/src/ObjectPool/src/IPooledObjectPolicy.cs +++ b/src/ObjectPool/src/IPooledObjectPolicy.cs @@ -3,10 +3,23 @@ namespace Microsoft.Extensions.ObjectPool { + /// + /// Represents a policy for managing pooled objects. + /// + /// The type of object which is being pooled. public interface IPooledObjectPolicy { + /// + /// Create a . + /// + /// The which was created. T Create(); + /// + /// 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. + /// + /// The object to return to the pool. + /// true if the object should be returned to the pool. false if it's not possible/desirable for the pool to keep the object. bool Return(T obj); } } diff --git a/src/ObjectPool/src/ObjectPool.cs b/src/ObjectPool/src/ObjectPool.cs index 691beae60c..0a82ed6f53 100644 --- a/src/ObjectPool/src/ObjectPool.cs +++ b/src/ObjectPool/src/ObjectPool.cs @@ -3,15 +3,31 @@ namespace Microsoft.Extensions.ObjectPool { + /// + /// A pool of objects. + /// + /// The type of objects to pool. public abstract class ObjectPool where T : class { + /// + /// Gets an object from the pool if one is available, otherwise creates one. + /// + /// A . public abstract T Get(); + /// + /// Return an object to the pool. + /// + /// The object to add to the pool. public abstract void Return(T obj); } + /// + /// Methods for creating instances. + /// public static class ObjectPool { + /// public static ObjectPool Create(IPooledObjectPolicy policy = null) where T : class, new() { var provider = new DefaultObjectPoolProvider(); diff --git a/src/ObjectPool/src/ObjectPoolProvider.cs b/src/ObjectPool/src/ObjectPoolProvider.cs index 909795dd35..6b8ca219ea 100644 --- a/src/ObjectPool/src/ObjectPoolProvider.cs +++ b/src/ObjectPool/src/ObjectPoolProvider.cs @@ -3,13 +3,24 @@ namespace Microsoft.Extensions.ObjectPool { + /// + /// A provider of instances. + /// public abstract class ObjectPoolProvider { + /// + /// Creates an . + /// + /// The type to create a pool for. public ObjectPool Create() where T : class, new() { return Create(new DefaultPooledObjectPolicy()); } + /// + /// Creates an with the given . + /// + /// The type to create a pool for. public abstract ObjectPool Create(IPooledObjectPolicy policy) where T : class; } } diff --git a/src/Shared/CommandLineUtils/Utilities/ArgumentEscaper.cs b/src/Shared/CommandLineUtils/Utilities/ArgumentEscaper.cs index 7b696c5175..92543e7f23 100644 --- a/src/Shared/CommandLineUtils/Utilities/ArgumentEscaper.cs +++ b/src/Shared/CommandLineUtils/Utilities/ArgumentEscaper.cs @@ -19,8 +19,8 @@ namespace Microsoft.Extensions.CommandLineUtils /// /// See https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ /// - /// - /// + /// The arguments to concatenate. + /// The escaped arguments, concatenated. public static string EscapeAndConcatenate(IEnumerable 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; } }