From 9098a47dbfede8c549633090fe931687a4e9ee8d Mon Sep 17 00:00:00 2001 From: David Fowler Date: Wed, 16 Oct 2019 22:04:29 -0700 Subject: [PATCH] Some minor clean up of Stream implementations (#14986) * Some minor clean up of Stream implementations - Use TaskToApm from corefx to implement Begin/End in streams - Use PipeReader.CopyToAsync(Stream) to implement CopyToAsync - Add more overrides on derived Streams in IIS --- src/Servers/IIS/IIS/src/Core/DuplexStream.cs | 20 +++ .../IIS/IIS/src/Core/HttpRequestStream.cs | 45 +++---- .../IIS/IIS/src/Core/HttpResponseStream.cs | 36 +----- .../IIS/IIS/src/Core/IISHttpContext.IO.cs | 13 +- .../Microsoft.AspNetCore.Server.IIS.csproj | 1 + .../src/Internal/Http/HttpRequestStream.cs | 80 ++---------- .../src/Internal/Http/HttpResponseStream.cs | 33 +---- ...soft.AspNetCore.Server.Kestrel.Core.csproj | 1 + .../Middleware/Internal/DuplexPipeStream.cs | 64 +-------- .../src/Middleware/Internal/LoggingStream.cs | 64 +-------- src/Shared/TaskToApm.cs | 121 ++++++++++++++++++ 11 files changed, 194 insertions(+), 284 deletions(-) create mode 100644 src/Shared/TaskToApm.cs diff --git a/src/Servers/IIS/IIS/src/Core/DuplexStream.cs b/src/Servers/IIS/IIS/src/Core/DuplexStream.cs index 8ff01c778f..73dbd4cbb9 100644 --- a/src/Servers/IIS/IIS/src/Core/DuplexStream.cs +++ b/src/Servers/IIS/IIS/src/Core/DuplexStream.cs @@ -60,9 +60,29 @@ namespace Microsoft.AspNetCore.Server.IIS.Core return _requestBody.ReadAsync(buffer, offset, count, cancellationToken); } + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return _requestBody.ReadAsync(buffer, cancellationToken); + } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { return _responseBody.WriteAsync(buffer, offset, count, cancellationToken); } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return _responseBody.WriteAsync(buffer, cancellationToken); + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _requestBody.CopyToAsync(destination, bufferSize, cancellationToken); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _responseBody.FlushAsync(cancellationToken); + } } } diff --git a/src/Servers/IIS/IIS/src/Core/HttpRequestStream.cs b/src/Servers/IIS/IIS/src/Core/HttpRequestStream.cs index 9fa5d7405e..8d90fd69a5 100644 --- a/src/Servers/IIS/IIS/src/Core/HttpRequestStream.cs +++ b/src/Servers/IIS/IIS/src/Core/HttpRequestStream.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.IO; using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; @@ -35,40 +36,12 @@ namespace Microsoft.AspNetCore.Server.IIS.Core public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = ReadAsync(buffer, offset, count, default(CancellationToken), state); - if (callback != null) - { - task.ContinueWith(t => callback.Invoke(t)); - } - return task; + return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state); } public override int EndRead(IAsyncResult asyncResult) { - return ((Task)asyncResult).GetAwaiter().GetResult(); - } - - private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) - { - var tcs = new TaskCompletionSource(state); - var task = ReadAsync(buffer, offset, count, cancellationToken); - task.ContinueWith((task2, state2) => - { - var tcs2 = (TaskCompletionSource)state2; - if (task2.IsCanceled) - { - tcs2.SetCanceled(); - } - else if (task2.IsFaulted) - { - tcs2.SetException(task2.Exception); - } - else - { - tcs2.SetResult(task2.Result); - } - }, tcs, cancellationToken); - return tcs.Task; + return TaskToApm.End(asyncResult); } public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -97,6 +70,18 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } } + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + try + { + await _body.CopyToAsync(destination, cancellationToken); + } + catch (ConnectionAbortedException ex) + { + throw new TaskCanceledException("The request was aborted", ex); + } + } + public void StartAcceptingReads(IISHttpContext body) { // Only start if not aborted diff --git a/src/Servers/IIS/IIS/src/Core/HttpResponseStream.cs b/src/Servers/IIS/IIS/src/Core/HttpResponseStream.cs index 739b6b16f5..b1d0c1f22a 100644 --- a/src/Servers/IIS/IIS/src/Core/HttpResponseStream.cs +++ b/src/Servers/IIS/IIS/src/Core/HttpResponseStream.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core public override void Flush() { - FlushAsync(default(CancellationToken)).GetAwaiter().GetResult(); + FlushAsync(default).GetAwaiter().GetResult(); } public override Task FlushAsync(CancellationToken cancellationToken) @@ -41,45 +41,17 @@ namespace Microsoft.AspNetCore.Server.IIS.Core throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed); } - WriteAsync(buffer, offset, count, default(CancellationToken)).GetAwaiter().GetResult(); + WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult(); } public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = WriteAsync(buffer, offset, count, default(CancellationToken), state); - if (callback != null) - { - task.ContinueWith(t => callback.Invoke(t)); - } - return task; + return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state); } public override void EndWrite(IAsyncResult asyncResult) { - ((Task)asyncResult).GetAwaiter().GetResult(); - } - - private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) - { - var tcs = new TaskCompletionSource(state); - var task = WriteAsync(buffer, offset, count, cancellationToken); - task.ContinueWith((task2, state2) => - { - var tcs2 = (TaskCompletionSource)state2; - if (task2.IsCanceled) - { - tcs2.SetCanceled(); - } - else if (task2.IsFaulted) - { - tcs2.SetException(task2.Exception); - } - else - { - tcs2.SetResult(null); - } - }, tcs, cancellationToken); - return tcs.Task; + TaskToApm.End(asyncResult); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) diff --git a/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs b/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs index 68141058c5..5f831eb01a 100644 --- a/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs +++ b/src/Servers/IIS/IIS/src/Core/IISHttpContext.IO.cs @@ -3,11 +3,10 @@ using System; using System.Buffers; -using System.Net.Http; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.Server.IIS.Core @@ -54,6 +53,16 @@ namespace Microsoft.AspNetCore.Server.IIS.Core } } + internal Task CopyToAsync(Stream destination, CancellationToken cancellationToken) + { + if (!HasStartedConsumingRequestBody) + { + InitializeRequestIO(); + } + + return _bodyInputPipe.Reader.CopyToAsync(destination, cancellationToken); + } + /// /// Writes data to the output pipe. /// diff --git a/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj b/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj index 06fe1aa203..a95e4ec56a 100644 --- a/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj +++ b/src/Servers/IIS/IIS/src/Microsoft.AspNetCore.Server.IIS.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs index 4d5513293e..a1b30fb940 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpRequestStream.cs @@ -90,41 +90,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = ReadAsync(buffer, offset, count, default, state); - if (callback != null) - { - task.ContinueWith(t => callback.Invoke(t)); - } - return task; + return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state); } /// public override int EndRead(IAsyncResult asyncResult) { - return ((Task)asyncResult).GetAwaiter().GetResult(); - } - - private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) - { - var tcs = new TaskCompletionSource(state); - var task = ReadAsync(buffer, offset, count, cancellationToken); - task.ContinueWith((task2, state2) => - { - var tcs2 = (TaskCompletionSource)state2; - if (task2.IsCanceled) - { - tcs2.SetCanceled(); - } - else if (task2.IsFaulted) - { - tcs2.SetException(task2.Exception); - } - else - { - tcs2.SetResult(task2.Result); - } - }, tcs, cancellationToken); - return tcs.Task; + return TaskToApm.End(asyncResult); } private ValueTask ReadAsyncWrapper(Memory destination, CancellationToken cancellationToken) @@ -139,7 +111,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } } - private async ValueTask ReadAsyncInternal(Memory buffer, CancellationToken cancellationToken) + private async ValueTask ReadAsyncInternal(Memory destination, CancellationToken cancellationToken) { while (true) { @@ -150,19 +122,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http throw new OperationCanceledException("The read was canceled"); } - var readableBuffer = result.Buffer; - var readableBufferLength = readableBuffer.Length; + var buffer = result.Buffer; + var length = buffer.Length; - var consumed = readableBuffer.End; + var consumed = buffer.End; try { - if (readableBufferLength != 0) + if (length != 0) { - var actual = (int)Math.Min(readableBufferLength, buffer.Length); + var actual = (int)Math.Min(length, destination.Length); - var slice = actual == readableBufferLength ? readableBuffer : readableBuffer.Slice(0, actual); + var slice = actual == length ? buffer : buffer.Slice(0, actual); consumed = slice.End; - slice.CopyTo(buffer.Span); + slice.CopyTo(destination.Span); return actual; } @@ -193,37 +165,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http throw new ArgumentOutOfRangeException(nameof(bufferSize)); } - return CopyToAsyncInternal(destination, cancellationToken); - } - - private async Task CopyToAsyncInternal(Stream destination, CancellationToken cancellationToken) - { - while (true) - { - var result = await _pipeReader.ReadAsync(cancellationToken); - var readableBuffer = result.Buffer; - var readableBufferLength = readableBuffer.Length; - - try - { - if (readableBufferLength != 0) - { - foreach (var memory in readableBuffer) - { - await destination.WriteAsync(memory, cancellationToken); - } - } - - if (result.IsCompleted) - { - return; - } - } - finally - { - _pipeReader.AdvanceTo(readableBuffer.End); - } - } + return _pipeReader.CopyToAsync(destination, cancellationToken); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs index 2bf5017278..86ddb9b157 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpResponseStream.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; @@ -87,40 +86,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = WriteAsync(buffer, offset, count, default, state); - if (callback != null) - { - task.ContinueWith(t => callback.Invoke(t)); - } - return task; + return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state); } public override void EndWrite(IAsyncResult asyncResult) { - ((Task)asyncResult).GetAwaiter().GetResult(); - } - - private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) - { - var tcs = new TaskCompletionSource(state); - var task = WriteAsync(buffer, offset, count, cancellationToken); - task.ContinueWith((task2, state2) => - { - var tcs2 = (TaskCompletionSource)state2; - if (task2.IsCanceled) - { - tcs2.SetCanceled(); - } - else if (task2.IsFaulted) - { - tcs2.SetException(task2.Exception); - } - else - { - tcs2.SetResult(null); - } - }, tcs, cancellationToken); - return tcs.Task; + TaskToApm.End(asyncResult); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) diff --git a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj index ea3a87bad7..058df2190c 100644 --- a/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj +++ b/src/Servers/Kestrel/Core/src/Microsoft.AspNetCore.Server.Kestrel.Core.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Servers/Kestrel/Core/src/Middleware/Internal/DuplexPipeStream.cs b/src/Servers/Kestrel/Core/src/Middleware/Internal/DuplexPipeStream.cs index b81a5e8869..7a2dfdf362 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/Internal/DuplexPipeStream.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/Internal/DuplexPipeStream.cs @@ -152,78 +152,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = ReadAsync(buffer, offset, count, default, state); - if (callback != null) - { - task.ContinueWith(t => callback.Invoke(t)); - } - return task; + return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state); } public override int EndRead(IAsyncResult asyncResult) { - return ((Task)asyncResult).GetAwaiter().GetResult(); - } - - private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) - { - var tcs = new TaskCompletionSource(state); - var task = ReadAsync(buffer, offset, count, cancellationToken); - task.ContinueWith((task2, state2) => - { - var tcs2 = (TaskCompletionSource)state2; - if (task2.IsCanceled) - { - tcs2.SetCanceled(); - } - else if (task2.IsFaulted) - { - tcs2.SetException(task2.Exception); - } - else - { - tcs2.SetResult(task2.Result); - } - }, tcs, cancellationToken); - return tcs.Task; + return TaskToApm.End(asyncResult); } public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = WriteAsync(buffer, offset, count, default, state); - if (callback != null) - { - task.ContinueWith(t => callback.Invoke(t)); - } - return task; + return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state); } public override void EndWrite(IAsyncResult asyncResult) { - ((Task)asyncResult).GetAwaiter().GetResult(); - } - - private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) - { - var tcs = new TaskCompletionSource(state); - var task = WriteAsync(buffer, offset, count, cancellationToken); - task.ContinueWith((task2, state2) => - { - var tcs2 = (TaskCompletionSource)state2; - if (task2.IsCanceled) - { - tcs2.SetCanceled(); - } - else if (task2.IsFaulted) - { - tcs2.SetException(task2.Exception); - } - else - { - tcs2.SetResult(null); - } - }, tcs, cancellationToken); - return tcs.Task; + TaskToApm.End(asyncResult); } } } diff --git a/src/Servers/Kestrel/Core/src/Middleware/Internal/LoggingStream.cs b/src/Servers/Kestrel/Core/src/Middleware/Internal/LoggingStream.cs index e3fdec3f81..15758f64f4 100644 --- a/src/Servers/Kestrel/Core/src/Middleware/Internal/LoggingStream.cs +++ b/src/Servers/Kestrel/Core/src/Middleware/Internal/LoggingStream.cs @@ -176,78 +176,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal // The below APM methods call the underlying Read/WriteAsync methods which will still be logged. public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = ReadAsync(buffer, offset, count, default(CancellationToken), state); - if (callback != null) - { - task.ContinueWith(t => callback.Invoke(t)); - } - return task; + return TaskToApm.Begin(ReadAsync(buffer, offset, count), callback, state); } public override int EndRead(IAsyncResult asyncResult) { - return ((Task)asyncResult).GetAwaiter().GetResult(); - } - - private Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) - { - var tcs = new TaskCompletionSource(state); - var task = ReadAsync(buffer, offset, count, cancellationToken); - task.ContinueWith((task2, state2) => - { - var tcs2 = (TaskCompletionSource)state2; - if (task2.IsCanceled) - { - tcs2.SetCanceled(); - } - else if (task2.IsFaulted) - { - tcs2.SetException(task2.Exception); - } - else - { - tcs2.SetResult(task2.Result); - } - }, tcs, cancellationToken); - return tcs.Task; + return TaskToApm.End(asyncResult); } public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) { - var task = WriteAsync(buffer, offset, count, default(CancellationToken), state); - if (callback != null) - { - task.ContinueWith(t => callback.Invoke(t)); - } - return task; + return TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state); } public override void EndWrite(IAsyncResult asyncResult) { - ((Task)asyncResult).GetAwaiter().GetResult(); - } - - private Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken, object state) - { - var tcs = new TaskCompletionSource(state); - var task = WriteAsync(buffer, offset, count, cancellationToken); - task.ContinueWith((task2, state2) => - { - var tcs2 = (TaskCompletionSource)state2; - if (task2.IsCanceled) - { - tcs2.SetCanceled(); - } - else if (task2.IsFaulted) - { - tcs2.SetException(task2.Exception); - } - else - { - tcs2.SetResult(null); - } - }, tcs, cancellationToken); - return tcs.Task; + TaskToApm.End(asyncResult); } } } diff --git a/src/Shared/TaskToApm.cs b/src/Shared/TaskToApm.cs new file mode 100644 index 0000000000..3a05eb6b42 --- /dev/null +++ b/src/Shared/TaskToApm.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Helper methods for using Tasks to implement the APM pattern. +// +// Example usage, wrapping a Task-returning FooAsync method with Begin/EndFoo methods: +// +// public IAsyncResult BeginFoo(..., AsyncCallback callback, object state) => +// TaskToApm.Begin(FooAsync(...), callback, state); +// +// public int EndFoo(IAsyncResult asyncResult) => +// TaskToApm.End(asyncResult); + +#nullable enable +using System.Diagnostics; + +namespace System.Threading.Tasks +{ + /// + /// Provides support for efficiently using Tasks to implement the APM (Begin/End) pattern. + /// + internal static class TaskToApm + { + /// + /// Marshals the Task as an IAsyncResult, using the supplied callback and state + /// to implement the APM pattern. + /// + /// The Task to be marshaled. + /// The callback to be invoked upon completion. + /// The state to be stored in the IAsyncResult. + /// An IAsyncResult to represent the task's asynchronous operation. + public static IAsyncResult Begin(Task task, AsyncCallback? callback, object? state) => + new TaskAsyncResult(task, state, callback); + + /// Processes an IAsyncResult returned by Begin. + /// The IAsyncResult to unwrap. + public static void End(IAsyncResult asyncResult) + { + if (asyncResult is TaskAsyncResult twar) + { + twar._task.GetAwaiter().GetResult(); + return; + } + + throw new ArgumentNullException(); + } + + /// Processes an IAsyncResult returned by Begin. + /// The IAsyncResult to unwrap. + public static TResult End(IAsyncResult asyncResult) + { + if (asyncResult is TaskAsyncResult twar && twar._task is Task task) + { + return task.GetAwaiter().GetResult(); + } + + throw new ArgumentNullException(); + } + + /// Provides a simple IAsyncResult that wraps a Task. + /// + /// We could use the Task as the IAsyncResult if the Task's AsyncState is the same as the object state, + /// but that's very rare, in particular in a situation where someone cares about allocation, and always + /// using TaskAsyncResult simplifies things and enables additional optimizations. + /// + internal sealed class TaskAsyncResult : IAsyncResult + { + /// The wrapped Task. + internal readonly Task _task; + /// Callback to invoke when the wrapped task completes. + private readonly AsyncCallback? _callback; + + /// Initializes the IAsyncResult with the Task to wrap and the associated object state. + /// The Task to wrap. + /// The new AsyncState value. + /// Callback to invoke when the wrapped task completes. + internal TaskAsyncResult(Task task, object? state, AsyncCallback? callback) + { + Debug.Assert(task != null); + _task = task; + AsyncState = state; + + if (task.IsCompleted) + { + // Synchronous completion. Invoke the callback. No need to store it. + CompletedSynchronously = true; + callback?.Invoke(this); + } + else if (callback != null) + { + // Asynchronous completion, and we have a callback; schedule it. We use OnCompleted rather than ContinueWith in + // order to avoid running synchronously if the task has already completed by the time we get here but still run + // synchronously as part of the task's completion if the task completes after (the more common case). + _callback = callback; + _task.ConfigureAwait(continueOnCapturedContext: false) + .GetAwaiter() + .OnCompleted(InvokeCallback); // allocates a delegate, but avoids a closure + } + } + + /// Invokes the callback. + private void InvokeCallback() + { + Debug.Assert(!CompletedSynchronously); + Debug.Assert(_callback != null); + _callback.Invoke(this); + } + + /// Gets a user-defined object that qualifies or contains information about an asynchronous operation. + public object? AsyncState { get; } + /// Gets a value that indicates whether the asynchronous operation completed synchronously. + /// This is set lazily based on whether the has completed by the time this object is created. + public bool CompletedSynchronously { get; } + /// Gets a value that indicates whether the asynchronous operation has completed. + public bool IsCompleted => _task.IsCompleted; + /// Gets a that is used to wait for an asynchronous operation to complete. + public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle; + } + } +} \ No newline at end of file