diff --git a/src/Http/Http.Abstractions/src/HttpRequest.cs b/src/Http/Http.Abstractions/src/HttpRequest.cs index 4c4d0d1af15e..5f32c8c8d39e 100644 --- a/src/Http/Http.Abstractions/src/HttpRequest.cs +++ b/src/Http/Http.Abstractions/src/HttpRequest.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.IO; +using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; @@ -102,6 +103,11 @@ public abstract class HttpRequest /// The RequestBody Stream. public abstract Stream Body { get; set; } + /// + /// Gets or sets the request body pipe . + /// + public abstract PipeReader BodyPipe { get; set; } + /// /// Checks the Content-Type header for form types. /// diff --git a/src/Http/Http.Abstractions/src/HttpResponse.cs b/src/Http/Http.Abstractions/src/HttpResponse.cs index 8a1e5d490829..e5af10fbfa5e 100644 --- a/src/Http/Http.Abstractions/src/HttpResponse.cs +++ b/src/Http/Http.Abstractions/src/HttpResponse.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.IO.Pipelines; using System.Threading.Tasks; namespace Microsoft.AspNetCore.Http @@ -39,6 +40,11 @@ public abstract class HttpResponse /// public abstract Stream Body { get; set; } + /// + /// Gets or sets the response body pipe + /// + public abstract PipeWriter BodyPipe { get; set; } + /// /// Gets or sets the value for the Content-Length response header. /// diff --git a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj index 2f17e520197a..380a4bd95995 100644 --- a/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Http/Http.Abstractions/src/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -22,6 +22,7 @@ Microsoft.AspNetCore.Http.HttpResponse + diff --git a/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs b/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs new file mode 100644 index 000000000000..0c996ff6917d --- /dev/null +++ b/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs @@ -0,0 +1,18 @@ +// 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.IO.Pipelines; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Represents the HttpRequestBody as a PipeReader. + /// + public interface IRequestBodyPipeFeature + { + /// + /// A representing the request body, if any. + /// + PipeReader RequestBodyPipe { get; set; } + } +} diff --git a/src/Http/Http.Features/src/IResponseBodyPipeFeature.cs b/src/Http/Http.Features/src/IResponseBodyPipeFeature.cs new file mode 100644 index 000000000000..dd8bb798a974 --- /dev/null +++ b/src/Http/Http.Features/src/IResponseBodyPipeFeature.cs @@ -0,0 +1,19 @@ +// 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.Pipelines; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// Represents the HttpResponseBody as a PipeWriter + /// + public interface IResponseBodyPipeFeature + { + /// + /// A representing the response body, if any. + /// + PipeWriter ResponseBodyPipe { get; set; } + } +} diff --git a/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj b/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj index f1cf8b0b9eee..9b2b7f8f15d9 100644 --- a/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj +++ b/src/Http/Http.Features/src/Microsoft.AspNetCore.Http.Features.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Http/Http/src/Features/RequestBodyPipeFeature.cs b/src/Http/Http/src/Features/RequestBodyPipeFeature.cs new file mode 100644 index 000000000000..7f66fe9815a1 --- /dev/null +++ b/src/Http/Http/src/Features/RequestBodyPipeFeature.cs @@ -0,0 +1,49 @@ +// 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.Pipelines; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class RequestBodyPipeFeature : IRequestBodyPipeFeature + { + private StreamPipeReader _internalPipeReader; + private PipeReader _userSetPipeReader; + private HttpContext _context; + + public RequestBodyPipeFeature(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + _context = context; + } + + public PipeReader RequestBodyPipe + { + get + { + if (_userSetPipeReader != null) + { + return _userSetPipeReader; + } + + if (_internalPipeReader == null || + !object.ReferenceEquals(_internalPipeReader.InnerStream, _context.Request.Body)) + { + _internalPipeReader = new StreamPipeReader(_context.Request.Body); + _context.Response.RegisterForDispose(_internalPipeReader); + } + + return _internalPipeReader; + } + set + { + _userSetPipeReader = value ?? throw new ArgumentNullException(nameof(value)); + // TODO set the request body Stream to an adapted pipe https://github.com/aspnet/AspNetCore/issues/3971 + } + } + } +} diff --git a/src/Http/Http/src/Features/ResponseBodyPipeFeature.cs b/src/Http/Http/src/Features/ResponseBodyPipeFeature.cs new file mode 100644 index 000000000000..b0d9c8ffc4c4 --- /dev/null +++ b/src/Http/Http/src/Features/ResponseBodyPipeFeature.cs @@ -0,0 +1,49 @@ +// 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.Pipelines; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class ResponseBodyPipeFeature : IResponseBodyPipeFeature + { + private StreamPipeWriter _internalPipeWriter; + private PipeWriter _userSetPipeWriter; + private HttpContext _context; + + public ResponseBodyPipeFeature(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + _context = context; + } + + public PipeWriter ResponseBodyPipe + { + get + { + if (_userSetPipeWriter != null) + { + return _userSetPipeWriter; + } + + if (_internalPipeWriter == null || + !object.ReferenceEquals(_internalPipeWriter.InnerStream, _context.Response.Body)) + { + _internalPipeWriter = new StreamPipeWriter(_context.Response.Body); + _context.Response.RegisterForDispose(_internalPipeWriter); + } + + return _internalPipeWriter; + } + set + { + _userSetPipeWriter = value ?? throw new ArgumentNullException(nameof(value)); + // TODO set the response body Stream to an adapted pipe https://github.com/aspnet/AspNetCore/issues/3971 + } + } + } +} diff --git a/src/Http/Http/src/Internal/DefaultHttpRequest.cs b/src/Http/Http/src/Internal/DefaultHttpRequest.cs index cf8ac92a3b5f..4803942b9351 100644 --- a/src/Http/Http/src/Internal/DefaultHttpRequest.cs +++ b/src/Http/Http/src/Internal/DefaultHttpRequest.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; @@ -19,6 +20,7 @@ public class DefaultHttpRequest : HttpRequest private readonly static Func _newFormFeature = r => new FormFeature(r); private readonly static Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); private readonly static Func _newRouteValuesFeature = f => new RouteValuesFeature(); + private readonly static Func _newRequestBodyPipeFeature = context => new RequestBodyPipeFeature(context); private HttpContext _context; private FeatureReferences _features; @@ -57,6 +59,9 @@ public virtual void Uninitialize() private IRouteValuesFeature RouteValuesFeature => _features.Fetch(ref _features.Cache.RouteValues, _newRouteValuesFeature); + private IRequestBodyPipeFeature RequestBodyPipeFeature => + _features.Fetch(ref _features.Cache.BodyPipe, this.HttpContext, _newRequestBodyPipeFeature); + public override PathString PathBase { get { return new PathString(HttpRequestFeature.PathBase); } @@ -162,6 +167,12 @@ public override RouteValueDictionary RouteValues set { RouteValuesFeature.RouteValues = value; } } + public override PipeReader BodyPipe + { + get { return RequestBodyPipeFeature.RequestBodyPipe; } + set { RequestBodyPipeFeature.RequestBodyPipe = value; } + } + struct FeatureInterfaces { public IHttpRequestFeature Request; @@ -169,6 +180,7 @@ struct FeatureInterfaces public IFormFeature Form; public IRequestCookiesFeature Cookies; public IRouteValuesFeature RouteValues; + public IRequestBodyPipeFeature BodyPipe; } } } diff --git a/src/Http/Http/src/Internal/DefaultHttpResponse.cs b/src/Http/Http/src/Internal/DefaultHttpResponse.cs index 6a812426d850..b7ac14c25ae2 100644 --- a/src/Http/Http/src/Internal/DefaultHttpResponse.cs +++ b/src/Http/Http/src/Internal/DefaultHttpResponse.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.IO.Pipelines; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; @@ -14,6 +15,7 @@ public class DefaultHttpResponse : HttpResponse // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 private readonly static Func _nullResponseFeature = f => null; private readonly static Func _newResponseCookiesFeature = f => new ResponseCookiesFeature(f); + private readonly static Func _newResponseBodyPipeFeature = context => new ResponseBodyPipeFeature(context); private HttpContext _context; private FeatureReferences _features; @@ -41,6 +43,8 @@ public virtual void Uninitialize() private IResponseCookiesFeature ResponseCookiesFeature => _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature); + private IResponseBodyPipeFeature ResponseBodyPipeFeature => + _features.Fetch(ref _features.Cache.BodyPipe, this.HttpContext, _newResponseBodyPipeFeature); public override HttpContext HttpContext { get { return _context; } } @@ -96,6 +100,12 @@ public override bool HasStarted get { return HttpResponseFeature.HasStarted; } } + public override PipeWriter BodyPipe + { + get { return ResponseBodyPipeFeature.ResponseBodyPipe; } + set { ResponseBodyPipeFeature.ResponseBodyPipe = value; } + } + public override void OnStarting(Func callback, object state) { if (callback == null) @@ -134,6 +144,7 @@ struct FeatureInterfaces { public IHttpResponseFeature Response; public IResponseCookiesFeature Cookies; + public IResponseBodyPipeFeature BodyPipe; } } } diff --git a/src/Http/Http/src/Internal/ReusableHttpRequest.cs b/src/Http/Http/src/Internal/ReusableHttpRequest.cs index f491acd41c23..2bbd1f56fa23 100644 --- a/src/Http/Http/src/Internal/ReusableHttpRequest.cs +++ b/src/Http/Http/src/Internal/ReusableHttpRequest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Pipelines; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -18,6 +19,8 @@ public sealed class ReusableHttpRequest : HttpRequest private readonly static Func _newFormFeature = r => new FormFeature(r); private readonly static Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); private readonly static Func _newRouteValuesFeature = f => new RouteValuesFeature(); + private readonly static Func _newRequestBodyPipeFeature = context => new RequestBodyPipeFeature(context); + private HttpContext _context; private FeatureReferences _features; @@ -56,6 +59,9 @@ public void Uninitialize() private IRouteValuesFeature RouteValuesFeature => _features.Fetch(ref _features.Cache.RouteValues, _newRouteValuesFeature); + private IRequestBodyPipeFeature RequestBodyPipeFeature => + _features.Fetch(ref _features.Cache.BodyPipe, this.HttpContext, _newRequestBodyPipeFeature); + public override PathString PathBase { get { return new PathString(HttpRequestFeature.PathBase); } @@ -161,6 +167,12 @@ public override RouteValueDictionary RouteValues set { RouteValuesFeature.RouteValues = value; } } + public override PipeReader BodyPipe + { + get { return RequestBodyPipeFeature.RequestBodyPipe; } + set { RequestBodyPipeFeature.RequestBodyPipe = value; } + } + struct FeatureInterfaces { public IHttpRequestFeature Request; @@ -168,6 +180,7 @@ struct FeatureInterfaces public IFormFeature Form; public IRequestCookiesFeature Cookies; public IRouteValuesFeature RouteValues; + public IRequestBodyPipeFeature BodyPipe; } } } diff --git a/src/Http/Http/src/Internal/ReusableHttpResponse.cs b/src/Http/Http/src/Internal/ReusableHttpResponse.cs index fd816351a82d..5a6e25fea5f2 100644 --- a/src/Http/Http/src/Internal/ReusableHttpResponse.cs +++ b/src/Http/Http/src/Internal/ReusableHttpResponse.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Pipelines; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; @@ -13,6 +14,7 @@ public sealed class ReusableHttpResponse : HttpResponse // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 private readonly static Func _nullResponseFeature = f => null; private readonly static Func _newResponseCookiesFeature = f => new ResponseCookiesFeature(f); + private readonly static Func _newResponseBodyPipeFeature = context => new ResponseBodyPipeFeature(context); private HttpContext _context; private FeatureReferences _features; @@ -39,7 +41,8 @@ public void Uninitialize() private IResponseCookiesFeature ResponseCookiesFeature => _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature); - + private IResponseBodyPipeFeature ResponseBodyPipeFeature => + _features.Fetch(ref _features.Cache.BodyPipe, this.HttpContext, _newResponseBodyPipeFeature); public override HttpContext HttpContext { get { return _context; } } @@ -90,6 +93,12 @@ public override IResponseCookies Cookies get { return ResponseCookiesFeature.Cookies; } } + public override PipeWriter BodyPipe + { + get { return ResponseBodyPipeFeature.ResponseBodyPipe; } + set { ResponseBodyPipeFeature.ResponseBodyPipe = value; } + } + public override bool HasStarted { get { return HttpResponseFeature.HasStarted; } @@ -133,6 +142,7 @@ struct FeatureInterfaces { public IHttpResponseFeature Response; public IResponseCookiesFeature Cookies; + public IResponseBodyPipeFeature BodyPipe; } } } diff --git a/src/Http/Http/src/StreamPipeReader.cs b/src/Http/Http/src/StreamPipeReader.cs index cd62041bed39..9d9e64caca67 100644 --- a/src/Http/Http/src/StreamPipeReader.cs +++ b/src/Http/Http/src/StreamPipeReader.cs @@ -17,7 +17,7 @@ namespace System.IO.Pipelines /// /// Implements PipeReader using an underlying stream. /// - public class StreamPipeReader : PipeReader + public class StreamPipeReader : PipeReader, IDisposable { private readonly int _minimumSegmentSize; private readonly int _minimumReadThreshold; @@ -70,6 +70,11 @@ public StreamPipeReader(Stream readingStream, StreamPipeReaderOptions options) _pool = options.MemoryPool; } + /// + /// Gets the inner stream that is being read from. + /// + public Stream InnerStream => _readingStream; + /// public override void AdvanceTo(SequencePosition consumed) { diff --git a/src/Http/Http/src/StreamPipeWriter.cs b/src/Http/Http/src/StreamPipeWriter.cs index f93950feec98..6926f1e9b9c8 100644 --- a/src/Http/Http/src/StreamPipeWriter.cs +++ b/src/Http/Http/src/StreamPipeWriter.cs @@ -64,6 +64,11 @@ public StreamPipeWriter(Stream writingStream, int minimumSegmentSize, MemoryPool _pool = pool ?? MemoryPool.Shared; } + /// + /// Gets the inner stream that is being read from. + /// + public Stream InnerStream => _writingStream; + /// public override void Advance(int count) { diff --git a/src/Http/Http/test/Features/RequestBodyPipeFeatureTests.cs b/src/Http/Http/test/Features/RequestBodyPipeFeatureTests.cs new file mode 100644 index 000000000000..542b12899fa8 --- /dev/null +++ b/src/Http/Http/test/Features/RequestBodyPipeFeatureTests.cs @@ -0,0 +1,134 @@ +// 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; +using System.IO.Pipelines; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class RequestBodyPipeFeatureTests + { + [Fact] + public void RequestBodyReturnsStreamPipeReader() + { + var context = new DefaultHttpContext(); + var expectedStream = new MemoryStream(); + context.Request.Body = expectedStream; + + var provider = new RequestBodyPipeFeature(context); + + var pipeBody = provider.RequestBodyPipe; + + Assert.True(pipeBody is StreamPipeReader); + Assert.Equal(expectedStream, (pipeBody as StreamPipeReader).InnerStream); + } + + [Fact] + public async Task RequestBodyReadCanWorkWithPipe() + { + var expectedString = "abcdef"; + var provider = InitializeFeatureWithData(expectedString); + + var data = await provider.RequestBodyPipe.ReadAsync(); + Assert.Equal(expectedString, GetStringFromReadResult(data)); + } + + [Fact] + public void RequestBodySetPipeReaderReturnsSameValue() + { + var context = new DefaultHttpContext(); + + var provider = new RequestBodyPipeFeature(context); + + var pipeReader = new Pipe().Reader; + provider.RequestBodyPipe = pipeReader; + + Assert.Equal(pipeReader, provider.RequestBodyPipe); + } + + [Fact] + public void RequestBodySetPipeReadReturnsUserSetValueAlways() + { + var context = new DefaultHttpContext(); + + var provider = new RequestBodyPipeFeature(context); + + var expectedPipeReader = new Pipe().Reader; + provider.RequestBodyPipe = expectedPipeReader; + + // Because the user set the RequestBodyPipe, this will return the user set pipeReader + context.Request.Body = new MemoryStream(); + + Assert.Equal(expectedPipeReader, provider.RequestBodyPipe); + } + + [Fact] + public async Task RequestBodyDoesNotAffectUserSetPipe() + { + var expectedString = "abcdef"; + var provider = InitializeFeatureWithData("hahaha"); + provider.RequestBodyPipe = await GetPipeReaderWithData(expectedString); + + var data = await provider.RequestBodyPipe.ReadAsync(); + Assert.Equal(expectedString, GetStringFromReadResult(data)); + } + + [Fact] + public void RequestBodyGetPipeReaderAfterSettingBodyTwice() + { + var context = new DefaultHttpContext(); + + context.Request.Body = new MemoryStream(); + + var provider = new RequestBodyPipeFeature(context); + + var pipeBody = provider.RequestBodyPipe; + + // Requery the PipeReader after setting the body again. + var expectedStream = new MemoryStream(); + context.Request.Body = expectedStream; + pipeBody = provider.RequestBodyPipe; + + Assert.True(pipeBody is StreamPipeReader); + Assert.Equal(expectedStream, (pipeBody as StreamPipeReader).InnerStream); + } + + [Fact] + public async Task RequestBodyGetsDataFromSecondStream() + { + var context = new DefaultHttpContext(); + context.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes("hahaha")); + var provider = new RequestBodyPipeFeature(context); + var _ = provider.RequestBodyPipe; + + var expectedString = "abcdef"; + context.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes(expectedString)); + var data = await provider.RequestBodyPipe.ReadAsync(); + Assert.Equal(expectedString, GetStringFromReadResult(data)); + } + + private RequestBodyPipeFeature InitializeFeatureWithData(string input) + { + var context = new DefaultHttpContext(); + context.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes(input)); + return new RequestBodyPipeFeature(context); + } + + private static string GetStringFromReadResult(ReadResult data) + { + return Encoding.ASCII.GetString(data.Buffer.ToArray()); + } + + private async Task GetPipeReaderWithData(string input) + { + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(Encoding.ASCII.GetBytes(input)); + return pipe.Reader; + } + } +} diff --git a/src/Http/Http/test/Features/ResponseBodyPipeFeatureTests.cs b/src/Http/Http/test/Features/ResponseBodyPipeFeatureTests.cs new file mode 100644 index 000000000000..51aa7422201c --- /dev/null +++ b/src/Http/Http/test/Features/ResponseBodyPipeFeatureTests.cs @@ -0,0 +1,56 @@ +// 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.IO; +using System.IO.Pipelines; +using Xunit; + +namespace Microsoft.AspNetCore.Http.Features +{ + public class ResponseBodyPipeFeatureTests + { + [Fact] + public void ResponseBodyReturnsStreamPipeReader() + { + var context = new DefaultHttpContext(); + var expectedStream = new MemoryStream(); + context.Response.Body = expectedStream; + + var provider = new ResponseBodyPipeFeature(context); + + var pipeBody = provider.ResponseBodyPipe; + + Assert.True(pipeBody is StreamPipeWriter); + Assert.Equal(expectedStream, (pipeBody as StreamPipeWriter).InnerStream); + } + + [Fact] + public void ResponseBodySetPipeReaderReturnsSameValue() + { + var context = new DefaultHttpContext(); + var provider = new ResponseBodyPipeFeature(context); + + var pipeWriter = new Pipe().Writer; + provider.ResponseBodyPipe = pipeWriter; + + Assert.Equal(pipeWriter, provider.ResponseBodyPipe); + } + + [Fact] + public void ResponseBodyGetPipeWriterAfterSettingBodyTwice() + { + var context = new DefaultHttpContext(); + var expectedStream = new MemoryStream(); + context.Response.Body = new MemoryStream(); + + var provider = new ResponseBodyPipeFeature(context); + + var pipeBody = provider.ResponseBodyPipe; + context.Response.Body = expectedStream; + pipeBody = provider.ResponseBodyPipe; + + Assert.True(pipeBody is StreamPipeWriter); + Assert.Equal(expectedStream, (pipeBody as StreamPipeWriter).InnerStream); + } + } +} diff --git a/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs index 09e47a962e1c..5bdc9b14ce8f 100644 --- a/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs +++ b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.IO.Pipelines; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; @@ -241,6 +243,44 @@ public void RouteValues_GetAndSet() Assert.Empty(request.RouteValues); } + [Fact] + public void BodyPipe_CanGet() + { + var context = new DefaultHttpContext(); + var bodyPipe = context.Request.BodyPipe; + Assert.NotNull(bodyPipe); + } + + [Fact] + public void BodyPipe_CanSet() + { + var pipeReader = new Pipe().Reader; + var context = new DefaultHttpContext(); + + context.Request.BodyPipe = pipeReader; + + Assert.Equal(pipeReader, context.Request.BodyPipe); + } + + [Fact] + public void BodyPipe_WrapsStream() + { + var context = new DefaultHttpContext(); + var expectedStream = new MemoryStream(); + context.Request.Body = expectedStream; + + var bodyPipe = context.Request.BodyPipe as StreamPipeReader; + + Assert.Equal(expectedStream, bodyPipe.InnerStream); + } + + [Fact] + public void BodyPipe_ThrowsWhenSettingNull() + { + var context = new DefaultHttpContext(); + Assert.Throws(() => context.Request.BodyPipe = null); + } + private class CustomRouteValuesFeature : IRouteValuesFeature { public RouteValueDictionary RouteValues { get; set; } diff --git a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs index 4764c44a63e0..18c85701ec4f 100644 --- a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs +++ b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.IO.Pipelines; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Xunit; @@ -59,6 +61,44 @@ public void GetContentType_ReturnsNullIfHeaderDoesNotExist() Assert.Null(response.ContentType); } + [Fact] + public void BodyPipe_CanGet() + { + var response = new DefaultHttpContext(); + var bodyPipe = response.Response.BodyPipe; + + Assert.NotNull(bodyPipe); + } + + [Fact] + public void BodyPipe_CanSet() + { + var response = new DefaultHttpContext(); + var pipeWriter = new Pipe().Writer; + response.Response.BodyPipe = pipeWriter; + + Assert.Equal(pipeWriter, response.Response.BodyPipe); + } + + [Fact] + public void BodyPipe_WrapsStream() + { + var context = new DefaultHttpContext(); + var expectedStream = new MemoryStream(); + context.Response.Body = expectedStream; + + var bodyPipe = context.Response.BodyPipe as StreamPipeWriter; + + Assert.Equal(expectedStream, bodyPipe.InnerStream); + } + + [Fact] + public void BodyPipe_ThrowsWhenSettingNull() + { + var context = new DefaultHttpContext(); + Assert.Throws(() => context.Response.BodyPipe = null); + } + private static HttpResponse CreateResponse(IHeaderDictionary headers) { var context = new DefaultHttpContext(); diff --git a/src/Http/Http/test/PipeTest.cs b/src/Http/Http/test/PipeTest.cs index 1aa2e5e06ac2..01801b95187c 100644 --- a/src/Http/Http/test/PipeTest.cs +++ b/src/Http/Http/test/PipeTest.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.IO.Pipelines; +using System.Text; namespace System.IO.Pipelines.Tests { @@ -38,6 +39,12 @@ public byte[] Read() return ReadWithoutFlush(); } + public string ReadAsString() + { + Writer.FlushAsync().GetAwaiter().GetResult(); + return Encoding.ASCII.GetString(ReadWithoutFlush()); + } + public void Write(byte[] data) { MemoryStream.Write(data, 0, data.Length); diff --git a/src/Http/Http/test/StreamPipeReaderTests.cs b/src/Http/Http/test/StreamPipeReaderTests.cs index 97e8f3ab631a..4c94bb6ce359 100644 --- a/src/Http/Http/test/StreamPipeReaderTests.cs +++ b/src/Http/Http/test/StreamPipeReaderTests.cs @@ -1,11 +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.Buffers; using System.Collections.Generic; -using System.IO; -using System.IO.Pipelines; using System.Linq; using System.Text; using System.Threading; @@ -533,6 +530,91 @@ public void SetOptionsToNullThrows() Assert.Throws(() => new StreamPipeReader(MemoryStream, null)); } + [Fact] + public async Task UseBothStreamAndPipeToReadConfirmSameSize() + { + Write(new byte[8]); + var buffer = new byte[4]; + + MemoryStream.Read(buffer, 0, buffer.Length); + var readResult = await Reader.ReadAsync(); + + Assert.Equal(buffer, readResult.Buffer.ToArray()); + } + + [Fact] + public async Task UseStreamThenPipeToReadNoBytesLost() + { + CreateReader(minimumSegmentSize: 1, minimumReadThreshold: 1); + + var expectedString = WriteString("abcdef"); + var accumulatedResult = ""; + var buffer = new byte[1]; + + for (var i = 0; i < expectedString.Length / 2; i++) + { + // Read from stream then pipe to guarantee no bytes are lost. + accumulatedResult += ReadFromStreamAsString(buffer); + accumulatedResult += await ReadFromPipeAsString(); + } + + Assert.Equal(expectedString, accumulatedResult); + } + + [Fact] + public async Task UsePipeThenStreamToReadNoBytesLost() + { + CreateReader(minimumSegmentSize: 1, minimumReadThreshold: 1); + + var expectedString = WriteString("abcdef"); + var accumulatedResult = ""; + var buffer = new byte[1]; + + for (var i = 0; i < expectedString.Length / 2; i++) + { + // Read from pipe then stream to guarantee no bytes are lost. + accumulatedResult += await ReadFromPipeAsString(); + accumulatedResult += ReadFromStreamAsString(buffer); + } + + Assert.Equal(expectedString, accumulatedResult); + } + + [Fact] + public async Task UseBothStreamAndPipeToReadWithoutAdvance_StreamIgnoresAdvance() + { + var buffer = new byte[1]; + CreateReader(minimumSegmentSize: 1, minimumReadThreshold: 1); + + WriteString("abc"); + ReadFromStreamAsString(buffer); + var readResult = await Reader.ReadAsync(); + + // No Advance + // Next call to Stream.Read will get the next 4 bytes rather than the bytes already read by the pipe + Assert.Equal("c", ReadFromStreamAsString(buffer)); + } + + private async Task ReadFromPipeAsString() + { + var readResult = await Reader.ReadAsync(); + var result = Encoding.ASCII.GetString(readResult.Buffer.ToArray()); + Reader.AdvanceTo(readResult.Buffer.End); + return result; + } + + private string ReadFromStreamAsString(byte[] buffer) + { + var res = MemoryStream.Read(buffer, 0, buffer.Length); + return Encoding.ASCII.GetString(buffer); + } + + private string WriteString(string expectedString) + { + Write(Encoding.ASCII.GetBytes(expectedString)); + return expectedString; + } + private void CreateReader(int minimumSegmentSize = 16, int minimumReadThreshold = 4, MemoryPool memoryPool = null) { Reader = new StreamPipeReader(MemoryStream, @@ -566,7 +648,8 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, return await base.ReadAsync(buffer, offset, count, cancellationToken); } -#if NETCOREAPP2_2 + // Keeping as this code will eventually be ported to corefx +#if NETCOREAPP3_0 public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { await Task.Yield(); diff --git a/src/Http/Http/test/StreamPipeWriterTests.cs b/src/Http/Http/test/StreamPipeWriterTests.cs index e9bee842a14b..d51bca97726b 100644 --- a/src/Http/Http/test/StreamPipeWriterTests.cs +++ b/src/Http/Http/test/StreamPipeWriterTests.cs @@ -269,6 +269,64 @@ public async Task CancelPendingFlushLostOfCancellationsNoDataLost() Assert.Equal(16 * 10 * 2, Read().Length); } + [Fact] + public async Task UseBothStreamAndPipeToWrite() + { + await WriteStringToPipeWriter("a"); + WriteStringToStream("c"); + + Assert.Equal("ac", ReadAsString()); + } + + [Fact] + public async Task UsePipeThenStreamToWriteMultipleTimes() + { + var expectedString = "abcdef"; + for (var i = 0; i < expectedString.Length; i++) + { + if (i % 2 == 0) + { + WriteStringToStream(expectedString[i].ToString()); + } + else + { + await WriteStringToPipeWriter(expectedString[i].ToString()); + } + } + + Assert.Equal(expectedString, ReadAsString()); + } + + [Fact] + public async Task UseStreamThenPipeToWriteMultipleTimes() + { + var expectedString = "abcdef"; + for (var i = 0; i < expectedString.Length; i++) + { + if (i % 2 == 0) + { + await WriteStringToPipeWriter(expectedString[i].ToString()); + } + else + { + WriteStringToStream(expectedString[i].ToString()); + } + } + + Assert.Equal(expectedString, ReadAsString()); + } + + private void WriteStringToStream(string input) + { + var buffer = Encoding.ASCII.GetBytes(input); + MemoryStream.Write(buffer, 0, buffer.Length); + } + + private async Task WriteStringToPipeWriter(string input) + { + await Writer.WriteAsync(Encoding.ASCII.GetBytes(input)); + } + private async Task CheckWriteIsNotCanceled() { var flushResult = await Writer.WriteAsync(Encoding.ASCII.GetBytes("data")); @@ -291,7 +349,6 @@ private void CheckCanceledFlush() internal class HangingStream : MemoryStream { - public HangingStream() { } @@ -311,7 +368,9 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, await Task.Delay(30000, cancellationToken); return 0; } -#if NETCOREAPP2_2 + + // Keeping as this code will eventually be ported to corefx +#if NETCOREAPP3_0 public override async ValueTask ReadAsync(Memory destination, CancellationToken cancellationToken = default) { await Task.Delay(30000, cancellationToken); @@ -326,8 +385,8 @@ internal class SingleWriteStream : MemoryStream public bool AllowAllWrites { get; set; } - -#if NETCOREAPP2_2 + // Keeping as this code will eventually be ported to corefx +#if NETCOREAPP3_0 public override async ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken = default) { try diff --git a/src/Http/build.cmd b/src/Http/build.cmd new file mode 100644 index 000000000000..033fe6f61468 --- /dev/null +++ b/src/Http/build.cmd @@ -0,0 +1,3 @@ +@ECHO OFF +SET RepoRoot=%~dp0..\.. +%RepoRoot%\build.cmd -projects %~dp0\**\*.*proj %* diff --git a/src/Middleware/WebSockets/test/UnitTests/Microsoft.AspNetCore.WebSockets.Tests.csproj b/src/Middleware/WebSockets/test/UnitTests/Microsoft.AspNetCore.WebSockets.Tests.csproj index 428fc49b8f88..4e125127354d 100644 --- a/src/Middleware/WebSockets/test/UnitTests/Microsoft.AspNetCore.WebSockets.Tests.csproj +++ b/src/Middleware/WebSockets/test/UnitTests/Microsoft.AspNetCore.WebSockets.Tests.csproj @@ -5,8 +5,8 @@ - - + +