diff --git a/.gitignore b/.gitignore index 8557e8854a..a04b5e4753 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ scripts/tmp/ .tools/ src/**/global.json launchSettings.json +BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/src/Http/Http/perf/NoopStream.cs b/src/Http/Http/perf/NoopStream.cs new file mode 100644 index 0000000000..9dcfe8408f --- /dev/null +++ b/src/Http/Http/perf/NoopStream.cs @@ -0,0 +1,70 @@ +// 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.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + public class NoopStream : Stream + { + public override bool CanRead => true; + + public override bool CanSeek => throw new NotImplementedException(); + + public override bool CanWrite => true; + + public override long Length => throw new NotImplementedException(); + + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return 0; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.FromResult(0); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + return new ValueTask(0); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + } + + public override void Write(byte[] buffer, int offset, int count) + { + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default(CancellationToken)) + { + return default(ValueTask); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/src/Http/Http/perf/StreamPipeReaderBenchmark.cs b/src/Http/Http/perf/StreamPipeReaderBenchmark.cs new file mode 100644 index 0000000000..28940baac0 --- /dev/null +++ b/src/Http/Http/perf/StreamPipeReaderBenchmark.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Http +{ + public class StreamPipeReaderBenchmark + { + private StreamPipeReader _pipeReaderNoop; + private StreamPipeReader _pipeReaderHelloWorld; + private StreamPipeReader _pipeReaderHelloWorldAync; + + [IterationSetup] + public void Setup() + { + _pipeReaderNoop = new StreamPipeReader(new NoopStream()); + _pipeReaderHelloWorld = new StreamPipeReader(new HelloWorldStream()); + _pipeReaderHelloWorldAync = new StreamPipeReader(new HelloWorldAsyncStream()); + } + + [Benchmark] + public async Task ReadNoop() + { + await _pipeReaderNoop.ReadAsync(); + } + + [Benchmark] + public async Task ReadHelloWorld() + { + var result = await _pipeReaderHelloWorld.ReadAsync(); + _pipeReaderHelloWorld.AdvanceTo(result.Buffer.End); + } + + [Benchmark] + public async Task ReadHelloWorldAsync() + { + var result = await _pipeReaderHelloWorldAync.ReadAsync(); + _pipeReaderHelloWorldAync.AdvanceTo(result.Buffer.End); + } + + private class HelloWorldStream : NoopStream + { + private static byte[] bytes = Encoding.ASCII.GetBytes("Hello World"); + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + bytes.CopyTo(buffer, 0); + return Task.FromResult(11); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + bytes.CopyTo(buffer); + + return new ValueTask(11); + } + } + + private class HelloWorldAsyncStream : NoopStream + { + private static byte[] bytes = Encoding.ASCII.GetBytes("Hello World"); + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Task.Yield(); + bytes.CopyTo(buffer, 0); + return 11; + } + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + await Task.Yield(); + bytes.CopyTo(buffer); + return 11; + } + } + } +} diff --git a/src/Http/Http/perf/StreamPipeWriterBenchmark.cs b/src/Http/Http/perf/StreamPipeWriterBenchmark.cs index 705cb0d8af..b1bb04e8dc 100644 --- a/src/Http/Http/perf/StreamPipeWriterBenchmark.cs +++ b/src/Http/Http/perf/StreamPipeWriterBenchmark.cs @@ -1,10 +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.IO; using System.Text; -using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; @@ -35,55 +33,5 @@ namespace Microsoft.AspNetCore.Http { await _pipeWriter.WriteAsync(_largeWrite); } - - public class NoopStream : Stream - { - public override bool CanRead => false; - - public override bool CanSeek => throw new System.NotImplementedException(); - - public override bool CanWrite => true; - - public override long Length => throw new System.NotImplementedException(); - - public override long Position { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new System.NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new System.NotImplementedException(); - } - - public override void SetLength(long value) - { - } - - public override void Write(byte[] buffer, int offset, int count) - { - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default(CancellationToken)) - { - return default(ValueTask); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - } } } diff --git a/src/Http/Http/src/BufferSegment.cs b/src/Http/Http/src/BufferSegment.cs new file mode 100644 index 0000000000..f0dcdc5077 --- /dev/null +++ b/src/Http/Http/src/BufferSegment.cs @@ -0,0 +1,111 @@ +// 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.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.IO.Pipelines +{ + public sealed class BufferSegment : ReadOnlySequenceSegment + { + private IMemoryOwner _memoryOwner; + private BufferSegment _next; + private int _end; + + /// + /// The Start represents the offset into AvailableMemory where the range of "active" bytes begins. At the point when the block is leased + /// the Start is guaranteed to be equal to 0. The value of Start may be assigned anywhere between 0 and + /// AvailableMemory.Length, and must be equal to or less than End. + /// + public int Start { get; private set; } + + /// + /// The End represents the offset into AvailableMemory where the range of "active" bytes ends. At the point when the block is leased + /// the End is guaranteed to be equal to Start. The value of Start may be assigned anywhere between 0 and + /// Buffer.Length, and must be equal to or less than End. + /// + public int End + { + get => _end; + set + { + Debug.Assert(value - Start <= AvailableMemory.Length); + + _end = value; + Memory = AvailableMemory.Slice(Start, _end - Start); + } + } + + /// + /// Reference to the next block of data when the overall "active" bytes spans multiple blocks. At the point when the block is + /// leased Next is guaranteed to be null. Start, End, and Next are used together in order to create a linked-list of discontiguous + /// working memory. The "active" memory is grown when bytes are copied in, End is increased, and Next is assigned. The "active" + /// memory is shrunk when bytes are consumed, Start is increased, and blocks are returned to the pool. + /// + public BufferSegment NextSegment + { + get => _next; + set + { + _next = value; + Next = value; + } + } + + public void SetMemory(IMemoryOwner memoryOwner) + { + SetMemory(memoryOwner, 0, 0); + } + + public void SetMemory(IMemoryOwner memoryOwner, int start, int end) + { + _memoryOwner = memoryOwner; + + AvailableMemory = _memoryOwner.Memory; + + RunningIndex = 0; + Start = start; + End = end; + NextSegment = null; + } + + public void ResetMemory() + { + _memoryOwner.Dispose(); + _memoryOwner = null; + AvailableMemory = default; + } + + internal IMemoryOwner MemoryOwner => _memoryOwner; + + public Memory AvailableMemory { get; private set; } + + public int Length => End - Start; + + /// + /// The amount of writable bytes in this segment. It is the amount of bytes between Length and End + /// + public int WritableBytes + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AvailableMemory.Length - End; + } + + public void SetNext(BufferSegment segment) + { + Debug.Assert(segment != null); + Debug.Assert(Next == null); + + NextSegment = segment; + + segment = this; + + while (segment.Next != null) + { + segment.NextSegment.RunningIndex = segment.RunningIndex + segment.Length; + segment = segment.NextSegment; + } + } + } +} diff --git a/src/Http/Http/src/StreamPipeReader.cs b/src/Http/Http/src/StreamPipeReader.cs new file mode 100644 index 0000000000..b74ea5ac34 --- /dev/null +++ b/src/Http/Http/src/StreamPipeReader.cs @@ -0,0 +1,364 @@ +// 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.Buffers; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Implements PipeReader using an underlying stream. + /// + public class StreamPipeReader : PipeReader + { + private readonly int _minimumSegmentSize; + private readonly Stream _readingStream; + private readonly MemoryPool _pool; + + private CancellationTokenSource _internalTokenSource; + private bool _isCompleted; + private ExceptionDispatchInfo _exceptionInfo; + + private BufferSegment _readHead; + private int _readIndex; + + private BufferSegment _readTail; + private long _bufferedBytes; + private bool _examinedEverything; + private object _lock = new object(); + + private CancellationTokenSource InternalTokenSource + { + get + { + lock(_lock) + { + if (_internalTokenSource == null) + { + _internalTokenSource = new CancellationTokenSource(); + } + return _internalTokenSource; + } + } + set + { + _internalTokenSource = value; + } + + } + + /// + /// Creates a new StreamPipeReader. + /// + /// The stream to read from. + public StreamPipeReader(Stream readingStream) : this(readingStream, minimumSegmentSize: 4096) + { + } + + /// + /// Creates a new StreamPipeReader. + /// + /// The stream to read from. + /// The minimum segment size to return from ReadAsync. + /// + public StreamPipeReader(Stream readingStream, int minimumSegmentSize, MemoryPool pool = null) + { + _minimumSegmentSize = minimumSegmentSize; + _readingStream = readingStream; + _pool = pool ?? MemoryPool.Shared; + } + + /// + public override void AdvanceTo(SequencePosition consumed) + { + AdvanceTo(consumed, consumed); + } + + /// + public override void AdvanceTo(SequencePosition consumed, SequencePosition examined) + { + ThrowIfCompleted(); + + if (_readHead == null || _readTail == null) + { + ThrowHelper.ThrowInvalidOperationException_NoDataRead(); + } + + AdvanceTo((BufferSegment)consumed.GetObject(), consumed.GetInteger(), (BufferSegment)examined.GetObject(), examined.GetInteger()); + } + + private void AdvanceTo(BufferSegment consumedSegment, int consumedIndex, BufferSegment examinedSegment, int examinedIndex) + { + if (consumedSegment == null) + { + return; + } + + var returnStart = _readHead; + var returnEnd = consumedSegment; + + var consumedBytes = new ReadOnlySequence(returnStart, _readIndex, consumedSegment, consumedIndex).Length; + + _bufferedBytes -= consumedBytes; + + Debug.Assert(_bufferedBytes >= 0); + + _examinedEverything = false; + + if (examinedSegment == _readTail) + { + // If we examined everything, we force ReadAsync to actually read from the underlying stream + // instead of returning a ReadResult from TryRead. + _examinedEverything = examinedIndex == _readTail.End - _readTail.Start; + } + + // Three cases here: + // 1. All data is consumed. If so, we reset _readHead and _readTail to _readTail's original memory owner + // SetMemory on a IMemoryOwner will reset the internal Memory to be an empty segment + // 2. A segment is entirely consumed but there is still more data in nextSegments + // We are allowed to remove an extra segment. by setting returnEnd to be the next block. + // 3. We are in the middle of a segment. + // Move _readHead and _readIndex to consumedSegment and index + if (_bufferedBytes == 0) + { + _readTail.SetMemory(_readTail.MemoryOwner); + _readHead = _readTail; + returnEnd = _readTail; + _readIndex = 0; + } + else if (consumedIndex == returnEnd.Length) + { + var nextBlock = returnEnd.NextSegment; + _readHead = nextBlock; + _readIndex = 0; + returnEnd = nextBlock; + } + else + { + _readHead = consumedSegment; + _readIndex = consumedIndex; + } + + // Remove all blocks that are freed (except the last one) + while (returnStart != returnEnd) + { + returnStart.ResetMemory(); + returnStart = returnStart.NextSegment; + } + } + + /// + public override void CancelPendingRead() + { + InternalTokenSource.Cancel(); + } + + /// + public override void Complete(Exception exception = null) + { + if (_isCompleted) + { + return; + } + + _isCompleted = true; + if (exception != null) + { + _exceptionInfo = ExceptionDispatchInfo.Capture(exception); + } + + var segment = _readHead; + while (segment != null) + { + segment.ResetMemory(); + segment = segment.NextSegment; + } + } + + /// + public override void OnWriterCompleted(Action callback, object state) + { + throw new NotSupportedException("OnWriterCompleted is not supported"); + } + + /// + public override async ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + // TODO ReadyAsync needs to throw if there are overlapping reads. + ThrowIfCompleted(); + + // PERF: store InternalTokenSource locally to avoid querying it twice (which acquires a lock) + var tokenSource = InternalTokenSource; + if (TryReadInternal(tokenSource, out var readResult)) + { + return readResult; + } + + var reg = new CancellationTokenRegistration(); + if (cancellationToken.CanBeCanceled) + { + reg = cancellationToken.Register(state => ((StreamPipeReader)state).Cancel(), this); + } + + using (reg) + { + var isCanceled = false; + try + { + AllocateReadTail(); +#if NETCOREAPP2_2 + var length = await _readingStream.ReadAsync(_readTail.AvailableMemory.Slice(_readTail.End), tokenSource.Token); +#elif NETSTANDARD2_0 + if (!MemoryMarshal.TryGetArray(_readTail.AvailableMemory.Slice(_readTail.End), out var arraySegment)) + { + ThrowHelper.CreateInvalidOperationException_NoArrayFromMemory(); + } + + var length = await _readingStream.ReadAsync(arraySegment.Array, arraySegment.Offset, arraySegment.Count, tokenSource.Token); +#else +#error Target frameworks need to be updated. +#endif + Debug.Assert(length + _readTail.End <= _readTail.AvailableMemory.Length); + + _readTail.End += length; + _bufferedBytes += length; + } + catch (OperationCanceledException) + { + ClearCancellationToken(); + + if (cancellationToken.IsCancellationRequested) + { + throw; + } + + isCanceled = true; + } + + return new ReadResult(GetCurrentReadOnlySequence(), isCanceled, IsCompletedOrThrow()); + } + } + + private void ClearCancellationToken() + { + lock(_lock) + { + _internalTokenSource = null; + } + } + + private void ThrowIfCompleted() + { + if (_isCompleted) + { + ThrowHelper.ThrowInvalidOperationException_NoReadingAllowed(); + } + } + + public override bool TryRead(out ReadResult result) + { + ThrowIfCompleted(); + + return TryReadInternal(InternalTokenSource, out result); + } + + private bool TryReadInternal(CancellationTokenSource source, out ReadResult result) + { + var isCancellationRequested = source.IsCancellationRequested; + if (isCancellationRequested || _bufferedBytes > 0 && !_examinedEverything) + { + // If TryRead/ReadAsync are called and cancellation is requested, we need to make sure memory is allocated for the ReadResult, + // otherwise if someone calls advance afterward on the ReadResult, it will throw. + if (isCancellationRequested) + { + AllocateReadTail(); + + ClearCancellationToken(); + } + + result = new ReadResult( + GetCurrentReadOnlySequence(), + isCanceled: isCancellationRequested, + IsCompletedOrThrow()); + return true; + } + + result = new ReadResult(); + return false; + } + + private ReadOnlySequence GetCurrentReadOnlySequence() + { + return new ReadOnlySequence(_readHead, _readIndex, _readTail, _readTail.End - _readTail.Start); + } + + private void AllocateReadTail() + { + if (_readHead == null) + { + Debug.Assert(_readTail == null); + _readHead = CreateBufferSegment(); + _readHead.SetMemory(_pool.Rent(GetSegmentSize())); + _readTail = _readHead; + } + else if (_readTail.WritableBytes == 0) + { + CreateNewTailSegment(); + } + } + + private void CreateNewTailSegment() + { + var nextSegment = CreateBufferSegment(); + nextSegment.SetMemory(_pool.Rent(GetSegmentSize())); + _readTail.SetNext(nextSegment); + _readTail = nextSegment; + } + + private int GetSegmentSize() => Math.Min(_pool.MaxBufferSize, _minimumSegmentSize); + + private BufferSegment CreateBufferSegment() + { + // TODO this can pool buffer segment objects + return new BufferSegment(); + } + + private void Cancel() + { + InternalTokenSource.Cancel(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool IsCompletedOrThrow() + { + if (!_isCompleted) + { + return false; + } + if (_exceptionInfo != null) + { + ThrowLatchedException(); + } + return true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowLatchedException() + { + _exceptionInfo.Throw(); + } + + public void Dispose() + { + Complete(); + } + } +} diff --git a/src/Http/Http/src/StreamPipeWriter.cs b/src/Http/Http/src/StreamPipeWriter.cs index f232aa97cf..91162fb962 100644 --- a/src/Http/Http/src/StreamPipeWriter.cs +++ b/src/Http/Http/src/StreamPipeWriter.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; diff --git a/src/Http/Http/src/ThrowHelper.cs b/src/Http/Http/src/ThrowHelper.cs new file mode 100644 index 0000000000..e16db82913 --- /dev/null +++ b/src/Http/Http/src/ThrowHelper.cs @@ -0,0 +1,23 @@ +// 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.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Http +{ + internal static class ThrowHelper + { + public static void ThrowInvalidOperationException_NoReadingAllowed() => throw CreateInvalidOperationException_NoReadingAllowed(); + [MethodImpl(MethodImplOptions.NoInlining)] + public static Exception CreateInvalidOperationException_NoReadingAllowed() => new InvalidOperationException("Reading is not allowed after reader was completed."); + + public static void ThrowInvalidOperationException_NoArrayFromMemory() => throw CreateInvalidOperationException_NoArrayFromMemory(); + [MethodImpl(MethodImplOptions.NoInlining)] + public static Exception CreateInvalidOperationException_NoArrayFromMemory() => new InvalidOperationException("Could not get byte[] from Memory."); + + public static void ThrowInvalidOperationException_NoDataRead() => throw CreateInvalidOperationException_NoDataRead(); + [MethodImpl(MethodImplOptions.NoInlining)] + public static Exception CreateInvalidOperationException_NoDataRead() => new InvalidOperationException("No data has been read into the StreamPipeReader."); + } +} diff --git a/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj b/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj index 78ac0c0ff1..e8afc2b0c5 100644 --- a/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj +++ b/src/Http/Http/test/Microsoft.AspNetCore.Http.Tests.csproj @@ -1,7 +1,7 @@ - + - $(StandardTestTfms) + net461 true diff --git a/src/Http/Http/test/PipeTest.cs b/src/Http/Http/test/PipeTest.cs index 2e94e3a267..30049fe6c0 100644 --- a/src/Http/Http/test/PipeTest.cs +++ b/src/Http/Http/test/PipeTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -11,19 +11,25 @@ namespace Microsoft.AspNetCore.Http.Tests { protected const int MaximumSizeHigh = 65; + protected const int MinimumSegmentSize = 4096; + public MemoryStream MemoryStream { get; set; } public PipeWriter Writer { get; set; } + public PipeReader Reader { get; set; } + protected PipeTest() { MemoryStream = new MemoryStream(); - Writer = new StreamPipeWriter(MemoryStream, 4096, new TestMemoryPool()); + Writer = new StreamPipeWriter(MemoryStream, MinimumSegmentSize, new TestMemoryPool()); + Reader = new StreamPipeReader(MemoryStream, MinimumSegmentSize, new TestMemoryPool()); } public void Dispose() { Writer.Complete(); + Reader.Complete(); } public byte[] Read() @@ -32,6 +38,17 @@ namespace Microsoft.AspNetCore.Http.Tests return ReadWithoutFlush(); } + public void Write(byte[] data) + { + MemoryStream.Write(data, 0, data.Length); + MemoryStream.Position = 0; + } + + public void WriteWithoutPosition(byte[] data) + { + MemoryStream.Write(data, 0, data.Length); + } + public byte[] ReadWithoutFlush() { MemoryStream.Position = 0; diff --git a/src/Http/Http/test/PipeWriterTests.cs b/src/Http/Http/test/PipeWriterTests.cs index 0cc6dc012f..564ea88f08 100644 --- a/src/Http/Http/test/PipeWriterTests.cs +++ b/src/Http/Http/test/PipeWriterTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.Http.Tests { var span = Writer.GetSpan(0); - var secondSpan = Writer.GetSpan(8000); + var secondSpan = Writer.GetSpan(10000); Assert.False(span.SequenceEqual(secondSpan)); } diff --git a/src/Http/Http/test/ReadAsyncCancellationTests.cs b/src/Http/Http/test/ReadAsyncCancellationTests.cs new file mode 100644 index 0000000000..86a599c391 --- /dev/null +++ b/src/Http/Http/test/ReadAsyncCancellationTests.cs @@ -0,0 +1,128 @@ +// 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.Buffers; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public class ReadAsyncCancellationTests : PipeTest + { + [Fact] + public async Task AdvanceShouldResetStateIfReadCanceled() + { + Reader.CancelPendingRead(); + + var result = await Reader.ReadAsync(); + var buffer = result.Buffer; + Reader.AdvanceTo(buffer.End); + + Assert.False(result.IsCompleted); + Assert.True(result.IsCanceled); + Assert.True(buffer.IsEmpty); + } + + [Fact] + public async Task CancellingBeforeAdvance() + { + Write(Encoding.ASCII.GetBytes("Hello World")); + + var result = await Reader.ReadAsync(); + var buffer = result.Buffer; + + Assert.Equal(11, buffer.Length); + Assert.False(result.IsCanceled); + Assert.True(buffer.IsSingleSegment); + var array = new byte[11]; + buffer.First.Span.CopyTo(array); + Assert.Equal("Hello World", Encoding.ASCII.GetString(array)); + + Reader.CancelPendingRead(); + + Reader.AdvanceTo(buffer.End); + + var awaitable = Reader.ReadAsync(); + + Assert.True(awaitable.IsCompleted); + + result = await awaitable; + + Assert.True(result.IsCanceled); + + Reader.AdvanceTo(result.Buffer.Start, result.Buffer.Start); + } + + [Fact] + public async Task ReadAsyncWithNewCancellationTokenNotAffectedByPrevious() + { + Write(new byte[1]); + + var cancellationTokenSource1 = new CancellationTokenSource(); + var result = await Reader.ReadAsync(cancellationTokenSource1.Token); + Reader.AdvanceTo(result.Buffer.Start); + + cancellationTokenSource1.Cancel(); + var cancellationTokenSource2 = new CancellationTokenSource(); + + // Verifying that ReadAsync does not throw + result = await Reader.ReadAsync(cancellationTokenSource2.Token); + Reader.AdvanceTo(result.Buffer.Start); + } + + [Fact] + public async Task CancellingPendingReadBeforeReadAsync() + { + Reader.CancelPendingRead(); + + ReadResult result = await Reader.ReadAsync(); + ReadOnlySequence buffer = result.Buffer; + Reader.AdvanceTo(buffer.End); + + Assert.False(result.IsCompleted); + Assert.True(result.IsCanceled); + Assert.True(buffer.IsEmpty); + + byte[] bytes = Encoding.ASCII.GetBytes("Hello World"); + Write(bytes); + + result = await Reader.ReadAsync(); + buffer = result.Buffer; + + Assert.Equal(11, buffer.Length); + Assert.False(result.IsCanceled); + Assert.True(buffer.IsSingleSegment); + var array = new byte[11]; + buffer.First.Span.CopyTo(array); + Assert.Equal("Hello World", Encoding.ASCII.GetString(array)); + + Reader.AdvanceTo(buffer.Start, buffer.Start); + } + + [Fact] + public void ReadAsyncCompletedAfterPreCancellation() + { + Reader.CancelPendingRead(); + Write(new byte[] { 1, 2, 3 }); + + ValueTaskAwaiter awaitable = Reader.ReadAsync().GetAwaiter(); + + Assert.True(awaitable.IsCompleted); + + ReadResult result = awaitable.GetResult(); + + Assert.True(result.IsCanceled); + + awaitable = Reader.ReadAsync().GetAwaiter(); + + Assert.True(awaitable.IsCompleted); + + Reader.AdvanceTo(awaitable.GetResult().Buffer.End); + } + } +} diff --git a/src/Http/Http/test/StreamPipeReaderTests.cs b/src/Http/Http/test/StreamPipeReaderTests.cs new file mode 100644 index 0000000000..ed750488c1 --- /dev/null +++ b/src/Http/Http/test/StreamPipeReaderTests.cs @@ -0,0 +1,503 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipelines; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Tests +{ + public partial class StreamPipeReaderTests : PipeTest + { + [Fact] + public async Task CanRead() + { + Write(Encoding.ASCII.GetBytes("Hello World")); + var readResult = await Reader.ReadAsync(); + var buffer = readResult.Buffer; + + Assert.Equal(11, buffer.Length); + Assert.True(buffer.IsSingleSegment); + var array = new byte[11]; + buffer.First.Span.CopyTo(array); + Assert.Equal("Hello World", Encoding.ASCII.GetString(array)); + Reader.AdvanceTo(buffer.End); + } + + [Fact] + public async Task CanReadMultipleTimes() + { + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + var readResult = await Reader.ReadAsync(); + + Assert.Equal(MinimumSegmentSize, readResult.Buffer.Length); + Assert.True(readResult.Buffer.IsSingleSegment); + + Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + + readResult = await Reader.ReadAsync(); + Assert.Equal(MinimumSegmentSize * 2, readResult.Buffer.Length); + Assert.False(readResult.Buffer.IsSingleSegment); + + Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + + readResult = await Reader.ReadAsync(); + Assert.Equal(10000, readResult.Buffer.Length); + Assert.False(readResult.Buffer.IsSingleSegment); + + Reader.AdvanceTo(readResult.Buffer.End); + } + + [Fact] + public async Task ReadWithAdvance() + { + Write(new byte[10000]); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.End); + + readResult = await Reader.ReadAsync(); + Assert.Equal(MinimumSegmentSize, readResult.Buffer.Length); + Assert.True(readResult.Buffer.IsSingleSegment); + } + + [Fact] + public async Task ReadWithAdvanceDifferentSegmentSize() + { + Reader = new StreamPipeReader(MemoryStream, 4095, new TestMemoryPool()); + Write(new byte[10000]); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.End); + + readResult = await Reader.ReadAsync(); + Assert.Equal(4095, readResult.Buffer.Length); + Assert.True(readResult.Buffer.IsSingleSegment); + } + + [Fact] + public async Task ReadWithAdvanceSmallSegments() + { + Reader = new StreamPipeReader(MemoryStream, 16, new TestMemoryPool()); + Write(new byte[128]); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.End); + + readResult = await Reader.ReadAsync(); + Assert.Equal(16, readResult.Buffer.Length); + Assert.True(readResult.Buffer.IsSingleSegment); + } + + [Fact] + public async Task ReadConsumePartialReadAsyncCallsTryRead() + { + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.GetPosition(2048)); + + // Confirm readResults are the same. + var readResult2 = await Reader.ReadAsync(); + + var didRead = Reader.TryRead(out var readResult3); + + Assert.Equal(readResult2, readResult3); + } + + [Fact] + public async Task ReadConsumeEntireTryReadReturnsNothing() + { + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.End); + var didRead = Reader.TryRead(out readResult); + + Assert.False(didRead); + } + + [Fact] + public async Task ReadExaminePartialReadAsyncDoesNotReturnMoreBytes() + { + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.GetPosition(2048)); + + var readResult2 = await Reader.ReadAsync(); + + Assert.Equal(readResult, readResult2); + } + + [Fact] + public async Task ReadExamineEntireReadAsyncReturnsNewData() + { + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + + var readResult2 = await Reader.ReadAsync(); + Assert.NotEqual(readResult, readResult2); + } + + [Fact] + public async Task ReadCanBeCancelledViaProvidedCancellationToken() + { + var pipeReader = new StreamPipeReader(new HangingStream()); + var cts = new CancellationTokenSource(1); + await Task.Delay(1); + await Assert.ThrowsAsync(async () => await pipeReader.ReadAsync(cts.Token)); + } + + [Fact] + public async Task ReadCanBeCanceledViaCancelPendingReadWhenReadIsAsync() + { + var pipeReader = new StreamPipeReader(new HangingStream()); + + var result = new ReadResult(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var task = Task.Run(async () => + { + var writingTask = pipeReader.ReadAsync(); + tcs.SetResult(0); + result = await writingTask; + }); + await tcs.Task; + pipeReader.CancelPendingRead(); + await task; + + Assert.True(result.IsCanceled); + } + + [Fact] + public async Task ReadAsyncReturnsCanceledIfCanceledBeforeRead() + { + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + // Make sure state isn't used from before + for (var i = 0; i < 3; i++) + { + Reader.CancelPendingRead(); + var readResultTask = Reader.ReadAsync(); + Assert.True(readResultTask.IsCompleted); + var readResult = readResultTask.GetAwaiter().GetResult(); + Assert.True(readResult.IsCanceled); + readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.End); + } + } + + [Fact] + public async Task ReadAsyncReturnsCanceledInterleaved() + { + // Cancel and Read interleaved to confirm cancellations are independent + for (var i = 0; i < 3; i++) + { + Reader.CancelPendingRead(); + var readResultTask = Reader.ReadAsync(); + Assert.True(readResultTask.IsCompleted); + var readResult = readResultTask.GetAwaiter().GetResult(); + Assert.True(readResult.IsCanceled); + + readResult = await Reader.ReadAsync(); + Assert.False(readResult.IsCanceled); + } + } + + [Fact] + public async Task AdvanceWithEmptySequencePositionNoop() + { + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.Start); + var readResult2 = await Reader.ReadAsync(); + + Assert.Equal(readResult, readResult2); + } + + [Fact] + public async Task AdvanceToInvalidCursorThrows() + { + Write(new byte[100]); + + var result = await Reader.ReadAsync(); + var buffer = result.Buffer; + + Reader.AdvanceTo(buffer.End); + + Reader.CancelPendingRead(); + result = await Reader.ReadAsync(); + Assert.Throws(() => Reader.AdvanceTo(buffer.End)); + Reader.AdvanceTo(result.Buffer.End); + } + + [Fact] + public void AdvanceWithoutReadingWithValidSequencePosition() + { + var sequencePosition = new SequencePosition(new BufferSegment(), 5); + Assert.Throws(() => Reader.AdvanceTo(sequencePosition)); + } + + [Fact] + public async Task AdvanceMultipleSegments() + { + Reader = new StreamPipeReader(MemoryStream, 16, new TestMemoryPool()); + Write(new byte[128]); + + var result = await Reader.ReadAsync(); + Assert.Equal(16, result.Buffer.Length); + Reader.AdvanceTo(result.Buffer.Start, result.Buffer.End); + + var result2 = await Reader.ReadAsync(); + Assert.Equal(32, result2.Buffer.Length); + Reader.AdvanceTo(result.Buffer.End, result2.Buffer.End); + + var result3 = await Reader.ReadAsync(); + Assert.Equal(32, result3.Buffer.Length); + } + + [Fact] + public async Task AdvanceMultipleSegmentsEdgeCase() + { + Reader = new StreamPipeReader(MemoryStream, 16, new TestMemoryPool()); + Write(new byte[128]); + + var result = await Reader.ReadAsync(); + Reader.AdvanceTo(result.Buffer.Start, result.Buffer.End); + result = await Reader.ReadAsync(); + Reader.AdvanceTo(result.Buffer.Start, result.Buffer.End); + + var result2 = await Reader.ReadAsync(); + Assert.Equal(48, result2.Buffer.Length); + Reader.AdvanceTo(result.Buffer.End, result2.Buffer.End); + + var result3 = await Reader.ReadAsync(); + Assert.Equal(32, result3.Buffer.Length); + } + + [Fact] + public async Task CompleteReaderWithoutAdvanceDoesNotThrow() + { + Write(new byte[100]); + await Reader.ReadAsync(); + Reader.Complete(); + } + + [Fact] + public async Task AdvanceAfterCompleteThrows() + { + Write(new byte[100]); + var buffer = (await Reader.ReadAsync()).Buffer; + + Reader.Complete(); + + var exception = Assert.Throws(() => Reader.AdvanceTo(buffer.End)); + Assert.Equal("Reading is not allowed after reader was completed.", exception.Message); + } + + [Fact] + public async Task ReadBetweenBlocks() + { + var blockSize = 16; + Reader = new StreamPipeReader(MemoryStream, blockSize, new TestMemoryPool()); + + WriteWithoutPosition(Enumerable.Repeat((byte)'a', blockSize - 5).ToArray()); + Write(Encoding.ASCII.GetBytes("Hello World")); + + // ReadAsync will only return one chunk at a time, so Advance/ReadAsync to get two chunks + var result = await Reader.ReadAsync(); + Reader.AdvanceTo(result.Buffer.Start, result.Buffer.End); + result = await Reader.ReadAsync(); + + var buffer = result.Buffer; + Assert.False(buffer.IsSingleSegment); + var helloBuffer = buffer.Slice(blockSize - 5); + Assert.False(helloBuffer.IsSingleSegment); + var memory = new List>(); + foreach (var m in helloBuffer) + { + memory.Add(m); + } + + var spans = memory; + Reader.AdvanceTo(buffer.Start, buffer.Start); + + Assert.Equal(2, memory.Count); + var helloBytes = new byte[spans[0].Length]; + spans[0].Span.CopyTo(helloBytes); + var worldBytes = new byte[spans[1].Length]; + spans[1].Span.CopyTo(worldBytes); + Assert.Equal("Hello", Encoding.ASCII.GetString(helloBytes)); + Assert.Equal(" World", Encoding.ASCII.GetString(worldBytes)); + } + + [Fact] + public async Task ThrowsOnReadAfterCompleteReader() + { + Reader.Complete(); + + await Assert.ThrowsAsync(async () => await Reader.ReadAsync()); + } + + [Fact] + public void TryReadAfterCancelPendingReadReturnsTrue() + { + Reader.CancelPendingRead(); + + var gotData = Reader.TryRead(out var result); + + Assert.True(result.IsCanceled); + + Reader.AdvanceTo(result.Buffer.End); + } + + [Fact] + public void ReadAsyncWithDataReadyReturnsTaskWithValue() + { + Write(new byte[20]); + var task = Reader.ReadAsync(); + Assert.True(IsTaskWithResult(task)); + } + + [Fact] + public void CancelledReadAsyncReturnsTaskWithValue() + { + Reader.CancelPendingRead(); + var task = Reader.ReadAsync(); + Assert.True(IsTaskWithResult(task)); + } + + [Fact] + public async Task AdvancePastMinReadSizeReadAsyncReturnsMoreData() + { + Reader = new StreamPipeReader(MemoryStream, 16, new TestMemoryPool()); + Write(new byte[32]); + var result = await Reader.ReadAsync(); + Assert.Equal(16, result.Buffer.Length); + + Reader.AdvanceTo(result.Buffer.GetPosition(12), result.Buffer.End); + result = await Reader.ReadAsync(); + Assert.Equal(20, result.Buffer.Length); + } + + [Fact] + public async Task ExamineEverythingResetsAfterSuccessfulRead() + { + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + + var readResult2 = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult2.Buffer.GetPosition(2000)); + + var readResult3 = await Reader.ReadAsync(); + Assert.Equal(6192, readResult3.Buffer.Length); + } + + [Fact] + public async Task ReadMultipleTimesAdvanceFreesAppropriately() + { + var blockSize = 16; + var pool = new TestMemoryPool(); + Reader = new StreamPipeReader(MemoryStream, blockSize, pool); + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + for (var i = 0; i < 99; i++) + { + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + } + + var result = await Reader.ReadAsync(); + Reader.AdvanceTo(result.Buffer.End); + Assert.Equal(1, pool.GetRentCount()); + } + + [Fact] + public async Task AsyncReadWorks() + { + MemoryStream = new AsyncStream(); + Reader = new StreamPipeReader(MemoryStream, 16, new TestMemoryPool()); + Write(Encoding.ASCII.GetBytes(new string('a', 10000))); + + for (var i = 0; i < 99; i++) + { + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + } + + var result = await Reader.ReadAsync(); + Assert.Equal(1600, result.Buffer.Length); + Reader.AdvanceTo(result.Buffer.End); + } + + [Fact] + public async Task ConsumePartialBufferWorks() + { + Reader = new StreamPipeReader(MemoryStream, 16, new TestMemoryPool()); + Write(Encoding.ASCII.GetBytes(new string('a', 8))); + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.GetPosition(4), readResult.Buffer.End); + MemoryStream.Position = 0; + + readResult = await Reader.ReadAsync(); + var resultString = Encoding.ASCII.GetString(readResult.Buffer.ToArray()); + Assert.Equal(new string('a', 12), resultString); + Reader.AdvanceTo(readResult.Buffer.End); + } + + [Fact] + public async Task ConsumePartialBufferBetweenMultipleSegmentsWorks() + { + Reader = new StreamPipeReader(MemoryStream, 16, new TestMemoryPool()); + Write(Encoding.ASCII.GetBytes(new string('a', 8))); + var readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.GetPosition(4), readResult.Buffer.End); + MemoryStream.Position = 0; + + readResult = await Reader.ReadAsync(); + Reader.AdvanceTo(readResult.Buffer.Start, readResult.Buffer.End); + MemoryStream.Position = 0; + + readResult = await Reader.ReadAsync(); + var resultString = Encoding.ASCII.GetString(readResult.Buffer.ToArray()); + Assert.Equal(new string('a', 20), resultString); + + Reader.AdvanceTo(readResult.Buffer.End); + } + + private bool IsTaskWithResult(ValueTask task) + { + return task == new ValueTask(task.Result); + } + + private class AsyncStream : MemoryStream + { + private static byte[] bytes = Encoding.ASCII.GetBytes("Hello World"); + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + await Task.Yield(); + return await base.ReadAsync(buffer, offset, count, cancellationToken); + } + +#if NETCOREAPP2_2 + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + await Task.Yield(); + return await base.ReadAsync(buffer, cancellationToken); + } +#endif + } + } +} diff --git a/src/Http/Http/test/StreamPipeWriterTests.cs b/src/Http/Http/test/StreamPipeWriterTests.cs index 76d3b34fae..eb00303080 100644 --- a/src/Http/Http/test/StreamPipeWriterTests.cs +++ b/src/Http/Http/test/StreamPipeWriterTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Http.Tests [Fact] public async Task CanWriteAsyncMultipleTimesIntoSameBlock() { - await Writer.WriteAsync(new byte[] { 1 }); await Writer.WriteAsync(new byte[] { 2 }); await Writer.WriteAsync(new byte[] { 3 }); @@ -313,6 +312,13 @@ namespace Microsoft.AspNetCore.Http.Tests await Task.Delay(30000, cancellationToken); return 0; } +#if NETCOREAPP2_2 + public override async ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) + { + await Task.Delay(30000, cancellationToken); + return 0; + } +#endif } internal class SingleWriteStream : MemoryStream diff --git a/src/Http/Http/test/TestMemoryPool.cs b/src/Http/Http/test/TestMemoryPool.cs index c5dd647dd1..746248a1bc 100644 --- a/src/Http/Http/test/TestMemoryPool.cs +++ b/src/Http/Http/test/TestMemoryPool.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -13,16 +13,27 @@ namespace Microsoft.AspNetCore.Http.Tests { public class TestMemoryPool : MemoryPool { - private MemoryPool _pool = Shared; + private MemoryPool _pool; private bool _disposed; + private int _rentCount; + public TestMemoryPool() + { + _pool = new CustomMemoryPool(); + } public override IMemoryOwner Rent(int minBufferSize = -1) { CheckDisposed(); + _rentCount++; return new PooledMemory(_pool.Rent(minBufferSize), this); } + public int GetRentCount() + { + return _rentCount; + } + protected override void Dispose(bool disposing) { _disposed = true; @@ -65,6 +76,7 @@ namespace Microsoft.AspNetCore.Http.Tests protected override void Dispose(bool disposing) { + _pool._rentCount--; _pool.CheckDisposed(); } @@ -135,5 +147,58 @@ namespace Microsoft.AspNetCore.Http.Tests return _owner.Memory.Span; } } + + private class CustomMemoryPool : MemoryPool + { + public override int MaxBufferSize => int.MaxValue; + + public override IMemoryOwner Rent(int minimumBufferSize = -1) + { + if (minimumBufferSize == -1) + { + minimumBufferSize = 4096; + } + + return new ArrayMemoryPoolBuffer(minimumBufferSize); + } + + protected override void Dispose(bool disposing) + { + throw new NotImplementedException(); + } + + private sealed class ArrayMemoryPoolBuffer : IMemoryOwner + { + private T[] _array; + + public ArrayMemoryPoolBuffer(int size) + { + _array = new T[size]; + } + + public Memory Memory + { + get + { + T[] array = _array; + if (array == null) + { + throw new ObjectDisposedException(nameof(array)); + } + + return new Memory(array); + } + } + + public void Dispose() + { + T[] array = _array; + if (array != null) + { + _array = null; + } + } + } + } } -} \ No newline at end of file +} diff --git a/src/Http/HttpAbstractions.sln b/src/Http/HttpAbstractions.sln index 7a70d0015d..55cafe847a 100644 --- a/src/Http/HttpAbstractions.sln +++ b/src/Http/HttpAbstractions.sln @@ -5,59 +5,61 @@ VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authentication.Abstractions", "Authentication.Abstractions", "{587C3D55-6092-4B86-99F5-E9772C9C1ADB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Authentication.Abstractions", "Authentication.Abstractions\src\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "{565B7B00-96A1-49B8-9753-9E045C6527A2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Abstractions", "Authentication.Abstractions\src\Microsoft.AspNetCore.Authentication.Abstractions.csproj", "{565B7B00-96A1-49B8-9753-9E045C6527A2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authentication.Core", "Authentication.Core", "{B51F45A6-428F-40F4-897F-7C62C29EC39A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Authentication.Core", "Authentication.Core\src\Microsoft.AspNetCore.Authentication.Core.csproj", "{A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Core", "Authentication.Core\src\Microsoft.AspNetCore.Authentication.Core.csproj", "{A3DEE5E8-FC9D-4135-8CDB-24E5BF954F96}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Authentication.Core.Test", "Authentication.Core\test\Microsoft.AspNetCore.Authentication.Core.Test.csproj", "{21071749-4361-4CD0-B5ED-541C72326800}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Core.Test", "Authentication.Core\test\Microsoft.AspNetCore.Authentication.Core.Test.csproj", "{21071749-4361-4CD0-B5ED-541C72326800}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Headers", "Headers", "{FF334B62-1AE2-477C-B91B-B28F898DFC3A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Net.Http.Headers", "Headers\src\Microsoft.Net.Http.Headers.csproj", "{D2B2E73E-A3A4-4996-906C-6647CD7D2634}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Net.Http.Headers", "Headers\src\Microsoft.Net.Http.Headers.csproj", "{D2B2E73E-A3A4-4996-906C-6647CD7D2634}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Net.Http.Headers.Tests", "Headers\test\Microsoft.Net.Http.Headers.Tests.csproj", "{9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Net.Http.Headers.Tests", "Headers\test\Microsoft.Net.Http.Headers.Tests.csproj", "{9CE486B4-0BC6-4C71-AA7C-BD66E78E11CF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http", "Http", "{FB2DCA0F-EB9E-425B-ABBC-D543DBEC090F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http", "Http\src\Microsoft.AspNetCore.Http.csproj", "{E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http", "Http\src\Microsoft.AspNetCore.Http.csproj", "{E35F0A95-0016-4B4D-BB85-ADB4CFAD857F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Tests", "Http\test\Microsoft.AspNetCore.Http.Tests.csproj", "{D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Tests", "Http\test\Microsoft.AspNetCore.Http.Tests.csproj", "{D9155D31-0844-4ED6-AC7B-6C4C9DA6E891}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.Abstractions", "Http.Abstractions", "{28F3D5CC-1F8E-4E15-94C8-E432DFA0A702}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Abstractions", "Http.Abstractions\src\Microsoft.AspNetCore.Http.Abstractions.csproj", "{D079CD1C-A18F-4457-91BC-432577D2FD37}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Abstractions", "Http.Abstractions\src\Microsoft.AspNetCore.Http.Abstractions.csproj", "{D079CD1C-A18F-4457-91BC-432577D2FD37}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Abstractions.Tests", "Http.Abstractions\test\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj", "{C28045AC-FF16-468C-A1E8-EC192DA2EF19}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Abstractions.Tests", "Http.Abstractions\test\Microsoft.AspNetCore.Http.Abstractions.Tests.csproj", "{C28045AC-FF16-468C-A1E8-EC192DA2EF19}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.Extensions", "Http.Extensions", "{CCC61332-7D63-4DDB-B604-884670157624}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Extensions", "Http.Extensions\src\Microsoft.AspNetCore.Http.Extensions.csproj", "{C06F2A33-B887-46BB-8F51-2666EDBE5D38}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Extensions", "Http.Extensions\src\Microsoft.AspNetCore.Http.Extensions.csproj", "{C06F2A33-B887-46BB-8F51-2666EDBE5D38}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Extensions.Tests", "Http.Extensions\test\Microsoft.AspNetCore.Http.Extensions.Tests.csproj", "{BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Extensions.Tests", "Http.Extensions\test\Microsoft.AspNetCore.Http.Extensions.Tests.csproj", "{BC50C116-2F25-4BC9-BDDC-7B3BA4A0BA07}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Http.Features", "Http.Features", "{0B1B3E58-DA37-46D6-B791-47739EF27790}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Features", "Http.Features\src\Microsoft.AspNetCore.Http.Features.csproj", "{F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Features", "Http.Features\src\Microsoft.AspNetCore.Http.Features.csproj", "{F6DEA0F5-79D0-4BC9-BFC9-CA6360B8B4E6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.Features.Tests", "Http.Features\test\Microsoft.AspNetCore.Http.Features.Tests.csproj", "{5A64C915-7045-4100-B2CB-3A50BD854D2D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Features.Tests", "Http.Features\test\Microsoft.AspNetCore.Http.Features.Tests.csproj", "{5A64C915-7045-4100-B2CB-3A50BD854D2D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Owin", "Owin", "{4D5C4F16-5DC5-4244-A10F-08545126F61B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Owin", "Owin\src\Microsoft.AspNetCore.Owin.csproj", "{21624719-422E-4621-A17A-C6F10436F1FE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Owin", "Owin\src\Microsoft.AspNetCore.Owin.csproj", "{21624719-422E-4621-A17A-C6F10436F1FE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Owin.Tests", "Owin\test\Microsoft.AspNetCore.Owin.Tests.csproj", "{38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Owin.Tests", "Owin\test\Microsoft.AspNetCore.Owin.Tests.csproj", "{38EA14B3-17BB-44F4-A9EA-A8675E9BF1E4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{391FBA36-BEEB-411A-A588-3F83901C0C1A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp", "samples\SampleApp\SampleApp.csproj", "{2378049E-ABE9-4843-AAC7-A6C9E704463D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "samples\SampleApp\SampleApp.csproj", "{2378049E-ABE9-4843-AAC7-A6C9E704463D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebUtilities", "WebUtilities", "{80A090C8-ED02-4DE3-875A-30DCCDBD84BA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebUtilities", "WebUtilities\src\Microsoft.AspNetCore.WebUtilities.csproj", "{1A866315-5FD5-4F96-BFAC-1447E3CB4514}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebUtilities", "WebUtilities\src\Microsoft.AspNetCore.WebUtilities.csproj", "{1A866315-5FD5-4F96-BFAC-1447E3CB4514}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.WebUtilities.Tests", "WebUtilities\test\Microsoft.AspNetCore.WebUtilities.Tests.csproj", "{068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.WebUtilities.Tests", "WebUtilities\test\Microsoft.AspNetCore.WebUtilities.Tests.csproj", "{068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.Performance", "Http\perf\Microsoft.AspNetCore.Http.Performance.csproj", "{8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -68,9 +70,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {565B7B00-96A1-49B8-9753-9E045C6527A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {565B7B00-96A1-49B8-9753-9E045C6527A2}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -288,6 +287,21 @@ Global {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|x64.Build.0 = Release|Any CPU {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|x86.ActiveCfg = Release|Any CPU {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8}.Release|x86.Build.0 = Release|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Debug|x64.ActiveCfg = Debug|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Debug|x64.Build.0 = Debug|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Debug|x86.ActiveCfg = Debug|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Debug|x86.Build.0 = Debug|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Release|Any CPU.Build.0 = Release|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Release|x64.ActiveCfg = Release|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Release|x64.Build.0 = Release|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Release|x86.ActiveCfg = Release|Any CPU + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {565B7B00-96A1-49B8-9753-9E045C6527A2} = {587C3D55-6092-4B86-99F5-E9772C9C1ADB} @@ -308,5 +322,9 @@ Global {2378049E-ABE9-4843-AAC7-A6C9E704463D} = {391FBA36-BEEB-411A-A588-3F83901C0C1A} {1A866315-5FD5-4F96-BFAC-1447E3CB4514} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA} {068A1DA0-C7DF-4E3C-9933-4E79A141EFF8} = {80A090C8-ED02-4DE3-875A-30DCCDBD84BA} + {8C635944-51F0-4BB0-A89E-CA49A7D9BE7F} = {FB2DCA0F-EB9E-425B-ABBC-D543DBEC090F} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {39F96525-F455-424F-ABDB-33DB59861EA6} EndGlobalSection EndGlobal