From c91cc89ee32ed2160b71c964fbe1fe8b98b5829f Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 31 Jul 2015 16:30:31 -0700 Subject: [PATCH] ResponseBuffering middleware initial checkin. Restrict buffer to reset. Add sample. Cleanup. --- BasicMiddleware.sln | 25 +- .../Properties/launchSettings.json | 21 ++ .../ResponseBufferingSample.xproj | 19 ++ samples/ResponseBufferingSample/Startup.cs | 36 +++ samples/ResponseBufferingSample/project.json | 30 ++ .../BufferingWriteStream.cs | 218 +++++++++++++ .../HttpBufferingFeature.cs | 30 ++ .../Microsoft.AspNet.Buffering.xproj | 20 ++ .../ResponseBufferingMiddleware.cs | 66 ++++ .../ResponseBufferingMiddlewareExtensions.cs | 20 ++ .../SendFileFeatureWrapper.cs | 28 ++ src/Microsoft.AspNet.Buffering/project.json | 18 ++ .../Microsoft.AspNet.Buffering.Tests.xproj | 20 ++ .../ResponseBufferingMiddlewareTests.cs | 298 ++++++++++++++++++ .../project.json | 22 ++ 15 files changed, 870 insertions(+), 1 deletion(-) create mode 100644 samples/ResponseBufferingSample/Properties/launchSettings.json create mode 100644 samples/ResponseBufferingSample/ResponseBufferingSample.xproj create mode 100644 samples/ResponseBufferingSample/Startup.cs create mode 100644 samples/ResponseBufferingSample/project.json create mode 100644 src/Microsoft.AspNet.Buffering/BufferingWriteStream.cs create mode 100644 src/Microsoft.AspNet.Buffering/HttpBufferingFeature.cs create mode 100644 src/Microsoft.AspNet.Buffering/Microsoft.AspNet.Buffering.xproj create mode 100644 src/Microsoft.AspNet.Buffering/ResponseBufferingMiddleware.cs create mode 100644 src/Microsoft.AspNet.Buffering/ResponseBufferingMiddlewareExtensions.cs create mode 100644 src/Microsoft.AspNet.Buffering/SendFileFeatureWrapper.cs create mode 100644 src/Microsoft.AspNet.Buffering/project.json create mode 100644 test/Microsoft.AspNet.Buffering.Tests/Microsoft.AspNet.Buffering.Tests.xproj create mode 100644 test/Microsoft.AspNet.Buffering.Tests/ResponseBufferingMiddlewareTests.cs create mode 100644 test/Microsoft.AspNet.Buffering.Tests/project.json diff --git a/BasicMiddleware.sln b/BasicMiddleware.sln index ace965aa26..d589e2f092 100644 --- a/BasicMiddleware.sln +++ b/BasicMiddleware.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.23018.0 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.HttpOverrides", "src\Microsoft.AspNet.HttpOverrides\Microsoft.AspNet.HttpOverrides.xproj", "{517308C3-B477-4B01-B461-CAB9C10B6928}" EndProject @@ -16,6 +16,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json EndProjectSection EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Buffering", "src\Microsoft.AspNet.Buffering\Microsoft.AspNet.Buffering.xproj", "{2363D0DD-A3BF-437E-9B64-B33AE132D875}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Buffering.Tests", "test\Microsoft.AspNet.Buffering.Tests\Microsoft.AspNet.Buffering.Tests.xproj", "{F5F1D123-9C81-4A9E-8644-AA46B8E578FB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9587FE9F-5A17-42C4-8021-E87F59CECB98}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseBufferingSample", "samples\ResponseBufferingSample\ResponseBufferingSample.xproj", "{E5C55B80-7827-40EB-B661-32B0E0E431CA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,6 +38,18 @@ Global {D6341B92-3416-4F11-8DF4-CB274296175F}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6341B92-3416-4F11-8DF4-CB274296175F}.Release|Any CPU.ActiveCfg = Release|Any CPU {D6341B92-3416-4F11-8DF4-CB274296175F}.Release|Any CPU.Build.0 = Release|Any CPU + {2363D0DD-A3BF-437E-9B64-B33AE132D875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2363D0DD-A3BF-437E-9B64-B33AE132D875}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2363D0DD-A3BF-437E-9B64-B33AE132D875}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2363D0DD-A3BF-437E-9B64-B33AE132D875}.Release|Any CPU.Build.0 = Release|Any CPU + {F5F1D123-9C81-4A9E-8644-AA46B8E578FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5F1D123-9C81-4A9E-8644-AA46B8E578FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5F1D123-9C81-4A9E-8644-AA46B8E578FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5F1D123-9C81-4A9E-8644-AA46B8E578FB}.Release|Any CPU.Build.0 = Release|Any CPU + {E5C55B80-7827-40EB-B661-32B0E0E431CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5C55B80-7827-40EB-B661-32B0E0E431CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5C55B80-7827-40EB-B661-32B0E0E431CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5C55B80-7827-40EB-B661-32B0E0E431CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -37,5 +57,8 @@ Global GlobalSection(NestedProjects) = preSolution {517308C3-B477-4B01-B461-CAB9C10B6928} = {A5076D28-FA7E-4606-9410-FEDD0D603527} {D6341B92-3416-4F11-8DF4-CB274296175F} = {8437B0F3-3894-4828-A945-A9187F37631D} + {2363D0DD-A3BF-437E-9B64-B33AE132D875} = {A5076D28-FA7E-4606-9410-FEDD0D603527} + {F5F1D123-9C81-4A9E-8644-AA46B8E578FB} = {8437B0F3-3894-4828-A945-A9187F37631D} + {E5C55B80-7827-40EB-B661-32B0E0E431CA} = {9587FE9F-5A17-42C4-8021-E87F59CECB98} EndGlobalSection EndGlobal diff --git a/samples/ResponseBufferingSample/Properties/launchSettings.json b/samples/ResponseBufferingSample/Properties/launchSettings.json new file mode 100644 index 0000000000..0e57697bac --- /dev/null +++ b/samples/ResponseBufferingSample/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNET_ENV": "Development" + } + }, + "web": { + "commandName": "web", + "launchBrowser": true, + "launchUrl": "http://localhost:5000/" + }, + "kestrel": { + "commandName": "kestrel", + "launchBrowser": true, + "launchUrl": "http://localhost:5001/" + } + } +} \ No newline at end of file diff --git a/samples/ResponseBufferingSample/ResponseBufferingSample.xproj b/samples/ResponseBufferingSample/ResponseBufferingSample.xproj new file mode 100644 index 0000000000..9026942e3d --- /dev/null +++ b/samples/ResponseBufferingSample/ResponseBufferingSample.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + e5c55b80-7827-40eb-b661-32b0e0e431ca + ResponseBufferingSample + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 46823 + + + \ No newline at end of file diff --git a/samples/ResponseBufferingSample/Startup.cs b/samples/ResponseBufferingSample/Startup.cs new file mode 100644 index 0000000000..46626e8065 --- /dev/null +++ b/samples/ResponseBufferingSample/Startup.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; + +namespace ResponseBufferingSample +{ + public class Startup + { + // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IApplicationBuilder app) + { + app.UseResponseBuffering(); + app.Run(async (context) => + { + // Write some stuff + context.Response.ContentType = "text/other"; + await context.Response.WriteAsync("Hello World!"); + + // ... more work ... + + // Something went wrong and we want to replace the response + context.Response.StatusCode = 200; + context.Response.Headers.Clear(); + context.Response.Body.SetLength(0); + + // Try again + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Hi Bob!"); + }); + } + } +} diff --git a/samples/ResponseBufferingSample/project.json b/samples/ResponseBufferingSample/project.json new file mode 100644 index 0000000000..f43d9ec282 --- /dev/null +++ b/samples/ResponseBufferingSample/project.json @@ -0,0 +1,30 @@ +{ + "webroot": "wwwroot", + "version": "1.0.0-*", + "dependencies": { + "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Buffering": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*" + }, + "commands": { + "web": "Microsoft.AspNet.Server.WebListener --server.urls=http://localhost:5000", + "kestrel": "Kestrel --server.urls=http://localhost:5001" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + }, + "publishExclude": [ + "node_modules", + "bower_components", + "**.xproj", + "**.user", + "**.vspscc" + ], + "exclude": [ + "wwwroot", + "node_modules", + "bower_components" + ] +} diff --git a/src/Microsoft.AspNet.Buffering/BufferingWriteStream.cs b/src/Microsoft.AspNet.Buffering/BufferingWriteStream.cs new file mode 100644 index 0000000000..d8a04d1cdc --- /dev/null +++ b/src/Microsoft.AspNet.Buffering/BufferingWriteStream.cs @@ -0,0 +1,218 @@ +// 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.AspNet.Buffering +{ + internal class BufferingWriteStream : Stream + { + private readonly Stream _innerStream; + private readonly MemoryStream _buffer = new MemoryStream(); + private bool _isBuffering = true; + + public BufferingWriteStream(Stream innerStream) + { + _innerStream = innerStream; + } + + public override bool CanRead + { + get { return false; } + } + + public override bool CanSeek + { + get { return _isBuffering; } + } + + public override bool CanWrite + { + get { return _innerStream.CanWrite; } + } + + public override long Length + { + get + { + if (_isBuffering) + { + return _buffer.Length; + } + // May throw + return _innerStream.Length; + } + } + + // Clear/Reset the buffer by setting Position, Seek, or SetLength to 0. Random access is not supported. + public override long Position + { + get + { + if (_isBuffering) + { + return _buffer.Position; + } + // May throw + return _innerStream.Position; + } + set + { + if (_isBuffering) + { + if (value != 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, nameof(Position) + " can only be set to 0."); + } + _buffer.Position = value; + _buffer.SetLength(value); + } + else + { + // May throw + _innerStream.Position = value; + } + } + } + + // Clear/Reset the buffer by setting Position, Seek, or SetLength to 0. Random access is not supported. + public override void SetLength(long value) + { + if (_isBuffering) + { + if (value != 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, nameof(Length) + " can only be set to 0."); + } + _buffer.Position = value; + _buffer.SetLength(value); + } + else + { + // May throw + _innerStream.SetLength(value); + } + } + + // Clear/Reset the buffer by setting Position, Seek, or SetLength to 0. Random access is not supported. + public override long Seek(long offset, SeekOrigin origin) + { + if (_isBuffering) + { + if (origin != SeekOrigin.Begin) + { + throw new ArgumentException(nameof(origin), nameof(Seek) + " can only be set to " + nameof(SeekOrigin.Begin) + "."); + } + if (offset != 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, nameof(Seek) + " can only be set to 0."); + } + _buffer.SetLength(offset); + return _buffer.Seek(offset, origin); + } + // Try the inner stream instead, but this will usually fail. + return _innerStream.Seek(offset, origin); + } + + internal void DisableBuffering() + { + _isBuffering = false; + if (_buffer.Length > 0) + { + Flush(); + } + } + + internal Task DisableBufferingAsync(CancellationToken cancellationToken) + { + _isBuffering = false; + if (_buffer.Length > 0) + { + return FlushAsync(cancellationToken); + } + return Task.FromResult(0); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_isBuffering) + { + _buffer.Write(buffer, offset, count); + } + else + { + _innerStream.Write(buffer, offset, count); + } + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_isBuffering) + { + return _buffer.WriteAsync(buffer, offset, count, cancellationToken); + } + else + { + return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } + } +#if !DNXCORE50 + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + if (_isBuffering) + { + return _buffer.BeginWrite(buffer, offset, count, callback, state); + } + else + { + return _innerStream.BeginWrite(buffer, offset, count, callback, state); + } + } + + public override void EndWrite(IAsyncResult asyncResult) + { + if (_isBuffering) + { + _buffer.EndWrite(asyncResult); + } + else + { + _innerStream.EndWrite(asyncResult); + } + } +#endif + public override void Flush() + { + _isBuffering = false; + if (_buffer.Length > 0) + { + _buffer.Seek(0, SeekOrigin.Begin); + _buffer.CopyTo(_innerStream); + _buffer.Seek(0, SeekOrigin.Begin); + _buffer.SetLength(0); + } + _innerStream.Flush(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + _isBuffering = false; + if (_buffer.Length > 0) + { + _buffer.Seek(0, SeekOrigin.Begin); + await _buffer.CopyToAsync(_innerStream, 1024 * 16, cancellationToken); + _buffer.Seek(0, SeekOrigin.Begin); + _buffer.SetLength(0); + } + await _innerStream.FlushAsync(cancellationToken); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("This Stream only supports Write operations."); + } + } +} diff --git a/src/Microsoft.AspNet.Buffering/HttpBufferingFeature.cs b/src/Microsoft.AspNet.Buffering/HttpBufferingFeature.cs new file mode 100644 index 0000000000..785f73e4e5 --- /dev/null +++ b/src/Microsoft.AspNet.Buffering/HttpBufferingFeature.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.AspNet.Http.Features; + +namespace Microsoft.AspNet.Buffering +{ + internal class HttpBufferingFeature : IHttpBufferingFeature + { + private readonly BufferingWriteStream _buffer; + private readonly IHttpBufferingFeature _innerFeature; + + internal HttpBufferingFeature(BufferingWriteStream buffer, IHttpBufferingFeature innerFeature) + { + _buffer = buffer; + _innerFeature = innerFeature; + } + + public void DisableRequestBuffering() + { + _innerFeature?.DisableRequestBuffering(); + } + + public void DisableResponseBuffering() + { + _buffer.DisableBuffering(); + _innerFeature?.DisableResponseBuffering(); + } + } +} diff --git a/src/Microsoft.AspNet.Buffering/Microsoft.AspNet.Buffering.xproj b/src/Microsoft.AspNet.Buffering/Microsoft.AspNet.Buffering.xproj new file mode 100644 index 0000000000..bc40210bfc --- /dev/null +++ b/src/Microsoft.AspNet.Buffering/Microsoft.AspNet.Buffering.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 2363d0dd-a3bf-437e-9b64-b33ae132d875 + Microsoft.AspNet.Buffering + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/Microsoft.AspNet.Buffering/ResponseBufferingMiddleware.cs b/src/Microsoft.AspNet.Buffering/ResponseBufferingMiddleware.cs new file mode 100644 index 0000000000..ec3a230146 --- /dev/null +++ b/src/Microsoft.AspNet.Buffering/ResponseBufferingMiddleware.cs @@ -0,0 +1,66 @@ +// 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.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; + +namespace Microsoft.AspNet.Buffering +{ + public class ResponseBufferingMiddleware + { + private readonly RequestDelegate _next; + + public ResponseBufferingMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext httpContext) + { + var originalResponseBody = httpContext.Response.Body; + + // no-op if buffering is already available. + if (originalResponseBody.CanSeek) + { + await _next(httpContext); + return; + } + + var originalBufferingFeature = httpContext.GetFeature(); + var originalSendFileFeature = httpContext.GetFeature(); + try + { + // Shim the response stream + var bufferStream = new BufferingWriteStream(originalResponseBody); + httpContext.Response.Body = bufferStream; + httpContext.SetFeature(new HttpBufferingFeature(bufferStream, originalBufferingFeature)); + if (originalSendFileFeature != null) + { + httpContext.SetFeature(new SendFileFeatureWrapper(originalSendFileFeature, bufferStream)); + } + + await _next(httpContext); + + // If we're still buffered, set the content-length header and flush the buffer. + // Only if the content-length header is not already set, and some content was buffered. + if (!httpContext.Response.HasStarted && bufferStream.CanSeek && bufferStream.Length > 0) + { + if (!httpContext.Response.ContentLength.HasValue) + { + httpContext.Response.ContentLength = bufferStream.Length; + } + await bufferStream.FlushAsync(); + } + } + finally + { + // undo everything + httpContext.SetFeature(originalBufferingFeature); + httpContext.SetFeature(originalSendFileFeature); + httpContext.Response.Body = originalResponseBody; + } + } + } +} diff --git a/src/Microsoft.AspNet.Buffering/ResponseBufferingMiddlewareExtensions.cs b/src/Microsoft.AspNet.Buffering/ResponseBufferingMiddlewareExtensions.cs new file mode 100644 index 0000000000..77a37d1658 --- /dev/null +++ b/src/Microsoft.AspNet.Buffering/ResponseBufferingMiddlewareExtensions.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.AspNet.Buffering; + +namespace Microsoft.AspNet.Builder +{ + public static class ResponseBufferingMiddlewareExtensions + { + /// + /// Enables full buffering of response bodies. This can be disabled on a per request basis using IHttpBufferingFeature. + /// + /// + /// + public static IApplicationBuilder UseResponseBuffering(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/Microsoft.AspNet.Buffering/SendFileFeatureWrapper.cs b/src/Microsoft.AspNet.Buffering/SendFileFeatureWrapper.cs new file mode 100644 index 0000000000..e3daff187f --- /dev/null +++ b/src/Microsoft.AspNet.Buffering/SendFileFeatureWrapper.cs @@ -0,0 +1,28 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Http.Features; + +namespace Microsoft.AspNet.Buffering +{ + internal class SendFileFeatureWrapper : IHttpSendFileFeature + { + private readonly IHttpSendFileFeature _originalSendFileFeature; + private readonly BufferingWriteStream _bufferStream; + + public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, BufferingWriteStream bufferStream) + { + _originalSendFileFeature = originalSendFileFeature; + _bufferStream = bufferStream; + } + + // Flush and disable the buffer if anyone tries to call the SendFile feature. + public async Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + await _bufferStream.DisableBufferingAsync(cancellation); + await _originalSendFileFeature.SendFileAsync(path, offset, length, cancellation); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Buffering/project.json b/src/Microsoft.AspNet.Buffering/project.json new file mode 100644 index 0000000000..9df583e0d5 --- /dev/null +++ b/src/Microsoft.AspNet.Buffering/project.json @@ -0,0 +1,18 @@ +{ + "version": "1.0.0-*", + "description": "ASP.NET middleware for buffering response bodies.", + "repository": { + "type": "git", + "url": "git://github.com/aspnet/basicmiddleware" + }, + "dependencies": { + "Microsoft.AspNet.Http.Abstractions": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + } + } + } +} diff --git a/test/Microsoft.AspNet.Buffering.Tests/Microsoft.AspNet.Buffering.Tests.xproj b/test/Microsoft.AspNet.Buffering.Tests/Microsoft.AspNet.Buffering.Tests.xproj new file mode 100644 index 0000000000..16b38cf3d8 --- /dev/null +++ b/test/Microsoft.AspNet.Buffering.Tests/Microsoft.AspNet.Buffering.Tests.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + f5f1d123-9c81-4a9e-8644-aa46b8e578fb + Microsoft.AspNet.Buffering.Tests + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/test/Microsoft.AspNet.Buffering.Tests/ResponseBufferingMiddlewareTests.cs b/test/Microsoft.AspNet.Buffering.Tests/ResponseBufferingMiddlewareTests.cs new file mode 100644 index 0000000000..ee7431ec28 --- /dev/null +++ b/test/Microsoft.AspNet.Buffering.Tests/ResponseBufferingMiddlewareTests.cs @@ -0,0 +1,298 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Buffering.Tests +{ + public class ResponseBufferingMiddlewareTests + { + [Fact] + public async Task BufferResponse_SetsContentLength() + { + var server = TestServer.Create(app => + { + app.UseResponseBuffering(); + app.Run(async context => + { + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + await context.Response.WriteAsync("Hello World"); + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + }); + }); + + var response = await server.CreateClient().GetAsync(""); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + + // Set automatically by buffer + IEnumerable values; + Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values)); + Assert.Equal("11", values.FirstOrDefault()); + } + + [Fact] + public async Task BufferResponseWithManualContentLength_NotReplaced() + { + var server = TestServer.Create(app => + { + app.UseResponseBuffering(); + app.Run(async context => + { + context.Response.ContentLength = 12; + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + await context.Response.WriteAsync("Hello World"); + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + }); + }); + + var response = await server.CreateClient().GetAsync(""); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + + IEnumerable values; + Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values)); + Assert.Equal("12", values.FirstOrDefault()); + } + + [Fact] + public async Task Seek_AllowsResttingBuffer() + { + var server = TestServer.Create(app => + { + app.UseResponseBuffering(); + app.Run(async context => + { + var body = context.Response.Body; + Assert.False(context.Response.HasStarted); + Assert.True(body.CanSeek); + Assert.Equal(0, body.Position); + Assert.Equal(0, body.Length); + + await context.Response.WriteAsync("Hello World"); + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + Assert.Equal(11, body.Position); + Assert.Equal(11, body.Length); + + Assert.Throws(() => body.Seek(1, SeekOrigin.Begin)); + Assert.Throws(() => body.Seek(0, SeekOrigin.Current)); + Assert.Throws(() => body.Seek(0, SeekOrigin.End)); + + Assert.Equal(0, body.Seek(0, SeekOrigin.Begin)); + Assert.Equal(0, body.Position); + Assert.Equal(0, body.Length); + + await context.Response.WriteAsync("12345"); + Assert.Equal(5, body.Position); + Assert.Equal(5, body.Length); + }); + }); + + var response = await server.CreateClient().GetAsync(""); + response.EnsureSuccessStatusCode(); + Assert.Equal("12345", await response.Content.ReadAsStringAsync()); + + // Set automatically by buffer + IEnumerable values; + Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values)); + Assert.Equal("5", values.FirstOrDefault()); + } + + [Fact] + public async Task SetPosition_AllowsResttingBuffer() + { + var server = TestServer.Create(app => + { + app.UseResponseBuffering(); + app.Run(async context => + { + var body = context.Response.Body; + Assert.False(context.Response.HasStarted); + Assert.True(body.CanSeek); + Assert.Equal(0, body.Position); + Assert.Equal(0, body.Length); + + await context.Response.WriteAsync("Hello World"); + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + Assert.Equal(11, body.Position); + Assert.Equal(11, body.Length); + + Assert.Throws(() => body.Position = 1); + + body.Position = 0; + Assert.Equal(0, body.Position); + Assert.Equal(0, body.Length); + + await context.Response.WriteAsync("12345"); + Assert.Equal(5, body.Position); + Assert.Equal(5, body.Length); + }); + }); + + var response = await server.CreateClient().GetAsync(""); + response.EnsureSuccessStatusCode(); + Assert.Equal("12345", await response.Content.ReadAsStringAsync()); + + // Set automatically by buffer + IEnumerable values; + Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values)); + Assert.Equal("5", values.FirstOrDefault()); + } + + [Fact] + public async Task SetLength_AllowsResttingBuffer() + { + var server = TestServer.Create(app => + { + app.UseResponseBuffering(); + app.Run(async context => + { + var body = context.Response.Body; + Assert.False(context.Response.HasStarted); + Assert.True(body.CanSeek); + Assert.Equal(0, body.Position); + Assert.Equal(0, body.Length); + + await context.Response.WriteAsync("Hello World"); + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + Assert.Equal(11, body.Position); + Assert.Equal(11, body.Length); + + Assert.Throws(() => body.SetLength(1)); + + body.SetLength(0); + Assert.Equal(0, body.Position); + Assert.Equal(0, body.Length); + + await context.Response.WriteAsync("12345"); + Assert.Equal(5, body.Position); + Assert.Equal(5, body.Length); + }); + }); + + var response = await server.CreateClient().GetAsync(""); + response.EnsureSuccessStatusCode(); + Assert.Equal("12345", await response.Content.ReadAsStringAsync()); + + // Set automatically by buffer + IEnumerable values; + Assert.True(response.Content.Headers.TryGetValues("Content-Length", out values)); + Assert.Equal("5", values.FirstOrDefault()); + } + + [Fact] + public async Task DisableBufferingViaFeature() + { + var server = TestServer.Create(app => + { + app.UseResponseBuffering(); + app.Run(async context => + { + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + + var bufferingFeature = context.GetFeature(); + Assert.NotNull(bufferingFeature); + bufferingFeature.DisableResponseBuffering(); + + Assert.False(context.Response.HasStarted); + Assert.False(context.Response.Body.CanSeek); + + await context.Response.WriteAsync("Hello World"); + + Assert.True(context.Response.HasStarted); + Assert.False(context.Response.Body.CanSeek); + }); + }); + + var response = await server.CreateClient().GetAsync(""); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + IEnumerable values; + Assert.False(response.Content.Headers.TryGetValues("Content-Length", out values)); + } + + [Fact] + public async Task DisableBufferingViaFeatureAfterFirstWrite_Flushes() + { + var server = TestServer.Create(app => + { + app.UseResponseBuffering(); + app.Run(async context => + { + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + + await context.Response.WriteAsync("Hello"); + + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + + var bufferingFeature = context.GetFeature(); + Assert.NotNull(bufferingFeature); + bufferingFeature.DisableResponseBuffering(); + + Assert.True(context.Response.HasStarted); + Assert.False(context.Response.Body.CanSeek); + + await context.Response.WriteAsync(" World"); + + Assert.True(context.Response.HasStarted); + Assert.False(context.Response.Body.CanSeek); + }); + }); + + var response = await server.CreateClient().GetAsync(""); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + IEnumerable values; + Assert.False(response.Content.Headers.TryGetValues("Content-Length", out values)); + } + + [Fact] + public async Task FlushDisablesBuffering() + { + var server = TestServer.Create(app => + { + app.UseResponseBuffering(); + app.Run(async context => + { + Assert.False(context.Response.HasStarted); + Assert.True(context.Response.Body.CanSeek); + + context.Response.Body.Flush(); + + Assert.True(context.Response.HasStarted); + Assert.False(context.Response.Body.CanSeek); + + await context.Response.WriteAsync("Hello World"); + + Assert.True(context.Response.HasStarted); + Assert.False(context.Response.Body.CanSeek); + }); + }); + + var response = await server.CreateClient().GetAsync(""); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello World", await response.Content.ReadAsStringAsync()); + IEnumerable values; + Assert.False(response.Content.Headers.TryGetValues("Content-Length", out values)); + } + } +} diff --git a/test/Microsoft.AspNet.Buffering.Tests/project.json b/test/Microsoft.AspNet.Buffering.Tests/project.json new file mode 100644 index 0000000000..516bc624f1 --- /dev/null +++ b/test/Microsoft.AspNet.Buffering.Tests/project.json @@ -0,0 +1,22 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Microsoft.AspNet.Buffering": "1.0.0-*", + "Microsoft.AspNet.TestHost": "1.0.0-*", + "xunit.runner.aspnet": "2.0.0-aspnet-*" + }, + "commands": { + "test": "xunit.runner.aspnet" + }, + + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Collections": "4.0.10-*", + "System.Linq": "4.0.0-*", + "System.Threading": "4.0.10-*" + } + } + } +}