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-*"
+ }
+ }
+ }
+}